Silicon ZombiesSILICON ZOMBIESDEVELOPER DOCSOpen preview

Mobile guide

Flutter integration

A small client pattern for public data, opaque sessions, safe errors, and server-authoritative membership.

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 stateClient presentation
access: freeRender normally for visitors and signed-in members.
access: vip, locked: trueKeep the card visible, add the lock and upgrade CTA, never reveal the protected URL.
VIP_REQUIREDOpen the membership CTA after an attempted protected action.
VIP_APPROVAL_REQUIREDShow 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');
}
Hosted checkout is also available.

Call POST /billing/checkout with { "plan": "monthly" | "annual" } and open the returned URL. Refresh GET /billing/status when the user returns.