Flutter integration
Use one REST client for the app. Its only concerns are JSON transport, opaque bearer tokens, timeouts, and safe API errors; product decisions remain on the server.
Client setup
Use http for requests, flutter_secure_storage for the session token, and a 20-second network timeout. The preview base URL already includes /api/v1.
class ApiException implements Exception {
ApiException(this.status, this.code, this.message, this.requestId);
final int status;
final String code;
final String message;
final String? requestId;
}
class ZombiesApi {
ZombiesApi(this.baseUrl, this.token);
final String baseUrl;
final String? token;
Future<dynamic> request(String method, String path, {Object? body}) async {
final request = http.Request(method, Uri.parse('$baseUrl$path'));
request.headers['Accept'] = 'application/json';
if (token != null) request.headers['Authorization'] = 'Bearer $token';
if (body != null) {
request.headers['Content-Type'] = 'application/json';
request.body = jsonEncode(body);
}
final response = await request.send().timeout(const Duration(seconds: 20));
final raw = await response.stream.bytesToString();
final json = raw.isEmpty ? <String, dynamic>{} : jsonDecode(raw) as Map<String, dynamic>;
if (response.statusCode >= 400) {
final error = json['error'] as Map<String, dynamic>? ?? const {};
throw ApiException(response.statusCode, error['code'] as String? ?? 'HTTP_ERROR',
error['message'] as String? ?? 'Request failed.', error['requestId'] as String?);
}
return json['data'];
}
}Membership UI states
| Server state | Client presentation |
|---|---|
access: free | Render normally for visitors and signed-in members. |
access: vip, locked: true | Keep the card visible, add the lock and upgrade CTA, never reveal the protected URL. |
VIP_REQUIRED | Open the membership CTA after an attempted protected action. |
VIP_APPROVAL_REQUIRED | Show the application status. The user must not see a payment flow yet. |
Audio and live chat
On play, request GET /content/:id/stream; use the redirect immediately and never persist the returned URL. For live chat, request POST /live/:id/chat-ticket only for an active VIP. Preview responses intentionally include a non-connectable ticket.
Mobile billing
The endpoint provides short-lived PaymentSheet material after authentication and VIP approval. iOS external billing is disabled by default and must only be enabled after confirming current App Store eligibility for the storefront.
Future<void> startVipPayment(ZombiesApi api, String plan, TargetPlatform platform) async {
final data = await api.request('POST', '/billing/mobile-payment-sheet', body: {
'plan': plan,
'platform': platform == TargetPlatform.iOS ? 'ios' : 'android',
}) as Map<String, dynamic>;
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
merchantDisplayName: 'Silicon Zombies',
paymentIntentClientSecret: data['paymentIntentClientSecret'] as String,
customerId: data['customerId'] as String,
customerEphemeralKeySecret: data['ephemeralKeySecret'] as String,
returnURL: 'siliconzombies://stripe-redirect',
style: ThemeMode.dark,
),
);
await Stripe.instance.presentPaymentSheet();
// The verified webhook, never this client callback, grants VIP access.
await api.request('GET', '/billing/status');
}Call POST /billing/checkout with { "plan": "monthly" | "annual" } and open the returned URL. Refresh GET /billing/status when the user returns.