diff --git a/developer-portal/components/ApiPlayground.tsx b/developer-portal/components/ApiPlayground.tsx new file mode 100644 index 00000000..d25d6d62 --- /dev/null +++ b/developer-portal/components/ApiPlayground.tsx @@ -0,0 +1,402 @@ +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + ScrollView, + ActivityIndicator, +} from 'react-native'; + +interface Endpoint { + id: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + path: string; + name: string; + hasBody?: boolean; + defaultBody?: string; +} + +const ENDPOINTS: Endpoint[] = [ + { id: 'list_sub', method: 'GET', path: '/v1/subscriptions', name: 'List Subscriptions' }, + { + id: 'create_sub', + method: 'POST', + path: '/v1/subscriptions', + name: 'Create Subscription', + hasBody: true, + defaultBody: JSON.stringify({ + name: "Netflix", + category: "streaming", + price: 15.99, + currency: "USD", + billingCycle: "monthly", + startDate: "2024-01-01T00:00:00Z" + }, null, 2) + }, + { id: 'list_pay', method: 'GET', path: '/v1/payments', name: 'List Payments' }, +]; + +const LANGUAGES = ['cURL', 'JavaScript', 'Python', 'Go']; + +export const ApiPlayground: React.FC = () => { + const [selectedEndpoint, setSelectedEndpoint] = useState(ENDPOINTS[0]); + const [apiKey, setApiKey] = useState('sk_test_your_api_key_here'); + const [requestBody, setRequestBody] = useState(ENDPOINTS[0].defaultBody || ''); + const [selectedLang, setSelectedLang] = useState('cURL'); + + const [response, setResponse] = useState<{ status: number | null, data: string | null }>({ status: null, data: null }); + const [loading, setLoading] = useState(false); + + const handleEndpointSelect = (endpoint: Endpoint) => { + setSelectedEndpoint(endpoint); + setRequestBody(endpoint.defaultBody || ''); + setResponse({ status: null, data: null }); + }; + + const generateCode = () => { + const url = `https://sandbox.api.subtrackr.io${selectedEndpoint.path}`; + const method = selectedEndpoint.method; + const bodyStr = selectedEndpoint.hasBody ? `\n -d '${requestBody}'` : ''; + const bodyJs = selectedEndpoint.hasBody ? `,\n body: JSON.stringify(${requestBody})` : ''; + const bodyPy = selectedEndpoint.hasBody ? `\npayload = ${requestBody}` : ''; + const bodyGo = selectedEndpoint.hasBody ? `\npayload := strings.NewReader(\`${requestBody}\`)` : ''; + + switch (selectedLang) { + case 'cURL': + return `curl -X ${method} ${url} \\ + -H "Authorization: Bearer ${apiKey}" \\ + -H "Content-Type: application/json"${bodyStr}`; + case 'JavaScript': + return `fetch("${url}", { + method: "${method}", + headers: { + "Authorization": "Bearer ${apiKey}", + "Content-Type": "application/json" + }${bodyJs} +}) +.then(response => response.json()) +.then(console.log);`; + case 'Python': + return `import requests +url = "${url}" +headers = { + "Authorization": "Bearer ${apiKey}", + "Content-Type": "application/json" +}${bodyPy} +response = requests.request("${method}", url, headers=headers${selectedEndpoint.hasBody ? ', json=payload' : ''}) +print(response.json())`; + case 'Go': + return `package main +import ( + "fmt" + "net/http" + "io/ioutil" + "strings" +) +func main() { + url := "${url}" + method := "${method}"${bodyGo} + client := &http.Client { } + req, _ := http.NewRequest(method, url, ${selectedEndpoint.hasBody ? 'payload' : 'nil'}) + req.Header.Add("Authorization", "Bearer ${apiKey}") + req.Header.Add("Content-Type", "application/json") + res, _ := client.Do(req) + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + fmt.Println(string(body)) +}`; + default: + return ''; + } + }; + + const handleExecute = async () => { + setLoading(true); + // Simulate network request to sandbox + setTimeout(() => { + let mockResponse = {}; + let mockStatus = 200; + + if (selectedEndpoint.id === 'list_sub') { + mockResponse = { + success: true, + data: [{ id: "sub_123", name: "Netflix", price: 15.99, status: "active" }], + pagination: { page: 1, limit: 20, total: 1 } + }; + } else if (selectedEndpoint.id === 'create_sub') { + try { + const bodyData = JSON.parse(requestBody); + mockResponse = { success: true, data: { id: "sub_new", ...bodyData, status: "active", createdAt: new Date().toISOString() } }; + mockStatus = 201; + } catch(e) { + mockResponse = { success: false, error: { code: "INVALID_REQUEST", message: "Invalid JSON body" } }; + mockStatus = 400; + } + } else { + mockResponse = { success: true, data: [] }; + } + + if (apiKey === '') { + mockResponse = { success: false, error: { code: "UNAUTHORIZED", message: "Missing API Key" } }; + mockStatus = 401; + } + + setResponse({ status: mockStatus, data: JSON.stringify(mockResponse, null, 2) }); + setLoading(false); + }, 800); + }; + + return ( + + Interactive API Playground + + + {/* Left Column - Configuration */} + + Endpoint + + {ENDPOINTS.map(ep => ( + handleEndpointSelect(ep)} + > + {ep.method} + {ep.name} + + ))} + + + API Key (Bearer Token) + + + {selectedEndpoint.hasBody && ( + <> + Request Body (JSON) + + + )} + + + {loading ? ( + + ) : ( + Execute Request + )} + + + + {/* Right Column - Code & Response */} + + + + {LANGUAGES.map(lang => ( + setSelectedLang(lang)} + > + {lang} + + ))} + + + {generateCode()} + + + + {response.data && ( + + + Response + + {response.status} + + + + {response.data} + + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + borderWidth: 1, + borderColor: '#E5E7EB', + padding: 20, + marginTop: 16, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + color: '#111827', + marginBottom: 20, + }, + layout: { + flexDirection: 'row', + flexWrap: 'wrap', + marginHorizontal: -10, + }, + configColumn: { + flex: 1, + minWidth: 300, + paddingHorizontal: 10, + marginBottom: 20, + }, + codeColumn: { + flex: 1, + minWidth: 300, + paddingHorizontal: 10, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#374151', + marginBottom: 8, + }, + endpointsScroll: { + flexDirection: 'row', + marginBottom: 20, + }, + endpointTab: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 8, + borderWidth: 1, + borderColor: '#E5E7EB', + marginRight: 8, + backgroundColor: '#F9FAFB', + }, + endpointTabSelected: { + borderColor: '#3B82F6', + backgroundColor: '#EFF6FF', + }, + endpointMethod: { + fontWeight: 'bold', + marginRight: 8, + fontSize: 12, + }, + endpointName: { + fontSize: 14, + color: '#4B5563', + }, + endpointNameSelected: { + color: '#1D4ED8', + fontWeight: '500', + }, + input: { + backgroundColor: '#F9FAFB', + borderWidth: 1, + borderColor: '#E5E7EB', + borderRadius: 8, + padding: 12, + fontSize: 14, + marginBottom: 16, + color: '#111827', + fontFamily: 'monospace', + }, + textArea: { + height: 120, + textAlignVertical: 'top', + }, + executeButton: { + backgroundColor: '#3B82F6', + paddingVertical: 12, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + executeButtonText: { + color: '#FFFFFF', + fontWeight: '600', + fontSize: 16, + }, + codeSection: { + marginBottom: 20, + }, + langTabs: { + flexDirection: 'row', + backgroundColor: '#1F2937', + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + paddingTop: 8, + paddingHorizontal: 8, + }, + langTab: { + paddingHorizontal: 16, + paddingVertical: 8, + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + }, + langTabSelected: { + backgroundColor: '#111827', + }, + langTabText: { + color: '#9CA3AF', + fontSize: 13, + fontWeight: '500', + }, + langTabTextSelected: { + color: '#F9FAFB', + }, + codeBlock: { + backgroundColor: '#111827', + padding: 16, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + codeText: { + color: '#D1D5DB', + fontFamily: 'monospace', + fontSize: 13, + lineHeight: 20, + }, + responseSection: { + marginTop: 8, + }, + responseHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + }, + statusBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + statusSuccess: { + backgroundColor: '#D1FAE5', + }, + statusError: { + backgroundColor: '#FEE2E2', + }, + statusText: { + fontSize: 12, + fontWeight: 'bold', + }, +}); diff --git a/developer-portal/docs/api-versioning.md b/developer-portal/docs/api-versioning.md new file mode 100644 index 00000000..0498644f --- /dev/null +++ b/developer-portal/docs/api-versioning.md @@ -0,0 +1,29 @@ +# API Versioning + +SubTrackr uses a URL-based versioning strategy to ensure backwards compatibility. The current active version is `v1`. + +## How it works + +When making API requests, you must include the version in the URL: + +```http +https://api.subtrackr.io/v1/subscriptions +``` + +## Backwards Compatibility + +We consider the following changes to be backwards-compatible: +- Adding new API endpoints +- Adding new properties to responses +- Adding optional request parameters +- Changing the order of properties in a response + +## Breaking Changes + +If we need to make backwards-incompatible changes, we will release a new API version (e.g., `v2`). Examples of breaking changes include: +- Removing an endpoint +- Removing or renaming a property in a response +- Changing a property's type +- Adding a new required parameter + +When a new version is released, we will provide an upgrade guide and deprecation timeline for the previous version. diff --git a/developer-portal/docs/changelog.md b/developer-portal/docs/changelog.md new file mode 100644 index 00000000..6f950c5d --- /dev/null +++ b/developer-portal/docs/changelog.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to the SubTrackr API and SDKs will be documented in this file. + +## [1.2.0] - 2026-07-24 + +### Added +- **API Playground**: Interactive API playground added to the developer portal for easy testing and code generation. +- **Python SDK**: Added support for pagination in the `list` method for subscriptions and payments. +- **Go SDK**: Added webhook verification helpers. + +### Changed +- Improved error messages for `INVALID_REQUEST` by providing more granular `details`. + +## [1.1.0] - 2026-06-15 + +### Added +- **Webhooks**: Added `invoice.generated` event. +- **API**: New `GET /v1/analytics/subscriptions` endpoint for aggregated subscription metrics. + +### Fixed +- Fixed an issue where the `category` filter on `GET /v1/subscriptions` was case-sensitive. + +## [1.0.0] - 2026-05-01 + +### Added +- Initial stable release of the SubTrackr API (`v1`). +- SDKs released for JavaScript/Node.js, Python, and Go. +- comprehensive Developer Portal with API Reference and Quick Start Guides. diff --git a/developer-portal/pages/DocumentationPage.tsx b/developer-portal/pages/DocumentationPage.tsx index eec1e30d..2d412b07 100644 --- a/developer-portal/pages/DocumentationPage.tsx +++ b/developer-portal/pages/DocumentationPage.tsx @@ -9,7 +9,7 @@ import { FlatList, } from 'react-native'; import { useDebounce } from '../../src/hooks/useDebounce'; - +import { ApiPlayground } from '../components/ApiPlayground'; interface DocSection { id: string; title: string; @@ -112,6 +112,23 @@ Events: Verify webhook signatures using HMAC-SHA256.`, }, + { + id: 'versioning', + title: 'API Versioning', + icon: '🔄', + content: `SubTrackr APIs are versioned. The current version is v1. +Always include the version in your request URL: +https://api.subtrackr.io/v1/ + +Backwards-incompatible changes will result in a new API version.`, + }, + { + id: 'changelog', + title: 'Changelog', + icon: '📝', + content: `Stay up-to-date with our latest API and SDK releases. +Check the Changelog documentation for detailed release notes.`, + }, ]; const QUICK_START_GUIDES: QuickStartGuide[] = [ @@ -218,6 +235,11 @@ export const DocumentationPage: React.FC = () => { /> + + Try the API + + + Quick Start Guides