Skip to content

Instantly share code, notes, and snippets.

@yeasin2002
Created January 29, 2026 04:02
Show Gist options
  • Select an option

  • Save yeasin2002/9150e41b48c71c7305c851818d2c90a0 to your computer and use it in GitHub Desktop.

Select an option

Save yeasin2002/9150e41b48c71c7305c851818d2c90a0 to your computer and use it in GitHub Desktop.
Frontend API Guide (Flutter)

Frontend API Guide (Flutter)

Version: 2.0.0
Last Updated: January 29, 2026
Target Platform: Flutter Mobile App
Status: ✅ Production Ready

📱 Integration Note: All API endpoints, request/response formats, and error codes in this guide are accurate and match the production backend. Code examples are provided in Dart/Flutter for mobile app integration.


Table of Contents

  1. Overview
  2. Authentication
  3. Wallet API
  4. Offer API
  5. Job API
  6. Transaction History API
  7. Stripe Integration
  8. Error Handling
  9. Best Practices

Overview

This document provides complete API reference for integrating the JobSphere payment system into your Flutter mobile application. It includes endpoints, request/response formats, and Flutter-specific implementation guidance.

Base URL

Production: https://api.jobsphere.com
Development: http://localhost:3000

Authentication

All endpoints except webhooks require JWT authentication:

headers: {
  'Authorization': 'Bearer ${accessToken}',
  'Content-Type': 'application/json',
}

Response Format

All API responses follow this structure:

Success Response:

{
  "status": 200,
  "message": "Success message",
  "data": { /* response data */ }
}

Error Response:

{
  "status": 400,
  "message": "Error message",
  "data": null,
  "errors": [
    {
      "field": "fieldName",
      "message": "Specific error"
    }
  ]
}

Authentication

Register

Endpoint: POST /api/auth/register

Request:

{
  "email": "[email protected]",
  "password": "SecurePass123",
  "name": "John Doe",
  "role": "customer"  // or "contractor"
}

Response:

{
  "status": 201,
  "message": "Registration successful",
  "data": {
    "user": {
      "_id": "507f1f77bcf86cd799439011",
      "email": "[email protected]",
      "name": "John Doe",
      "role": "customer"
    },
    "tokens": {
      "accessToken": "eyJhbGciOiJIUzI1NiIs...",
      "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
    }
  }
}

Login

Endpoint: POST /api/auth/login

Request:

{
  "email": "[email protected]",
  "password": "SecurePass123"
}

Response: Same as register


Wallet API

Get Wallet Balance

Endpoint: GET /api/wallet

Headers: Requires authentication

Response:

{
  "status": 200,
  "message": "Wallet retrieved successfully",
  "data": {
    "_id": "507f1f77bcf86cd799439012",
    "user": "507f1f77bcf86cd799439011",
    "balance": 150.00,
    "currency": "USD",
    "isActive": true,
    "isFrozen": false,
    "totalEarnings": 500.00,
    "totalSpent": 350.00,
    "totalWithdrawals": 100.00,
    "createdAt": "2026-01-15T10:00:00.000Z",
    "updatedAt": "2026-01-28T12:30:00.000Z"
  }
}

Flutter Implementation:

import 'package:http/http.dart' as http;
import 'dart:convert';

class WalletService {
  final String baseUrl;
  final String accessToken;

  WalletService({required this.baseUrl, required this.accessToken});

  Future<Map<String, dynamic>> getWallet() async {
    final response = await http.get(
      Uri.parse('$baseUrl/api/wallet'),
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Content-Type': 'application/json',
      },
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return data['data'];
    } else {
      throw Exception('Failed to load wallet');
    }
  }
}

Deposit Money

Endpoint: POST /api/wallet/deposit

Headers: Requires authentication

Request:

{
  "amount": 100.00
}

Response:

{
  "status": 200,
  "message": "Deposit initiated successfully",
  "data": {
    "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_...",
    "sessionId": "cs_test_a1b2c3d4e5f6",
    "amount": 100.00
  }
}

Flutter Implementation:

import 'package:url_launcher/url_launcher.dart';

class WalletService {
  // ... previous code

  Future<void> depositMoney(double amount) async {
    // Step 1: Create deposit request
    final response = await http.post(
      Uri.parse('$baseUrl/api/wallet/deposit'),
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Content-Type': 'application/json',
      },
      body: json.encode({'amount': amount}),
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body)['data'];
      final checkoutUrl = data['checkoutUrl'];
      
      // Step 2: Open Stripe Checkout in browser
      await _openStripeCheckout(checkoutUrl);
      
      // Step 3: Listen for completion (via polling or push notification)
      // See "Stripe Integration" section for details
    } else {
      throw Exception('Failed to create deposit');
    }
  }

  Future<void> _openStripeCheckout(String url) async {
    final Uri uri = Uri.parse(url);
    
    if (await canLaunchUrl(uri)) {
      // Opens in default browser (not in-app)
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    } else {
      throw Exception('Could not launch checkout URL');
    }
  }
}

Important Notes:

  • The backend returns a Stripe Checkout URL
  • Do NOT open Stripe UI in-app - use LaunchMode.externalApplication
  • User completes payment in their default browser
  • Payment completion is confirmed via webhook (backend handles this)
  • Frontend should poll wallet balance or listen for push notifications

Request Withdrawal (Contractors Only)

Endpoint: POST /api/wallet/withdraw

Headers: Requires authentication (contractor role)

Request:

{
  "amount": 50.00
}

Response:

{
  "status": 200,
  "message": "Withdrawal request submitted",
  "data": {
    "withdrawal": {
      "_id": "507f1f77bcf86cd799439013",
      "user": "507f1f77bcf86cd799439011",
      "amount": 50.00,
      "status": "pending",
      "requestedAt": "2026-01-28T12:00:00.000Z"
    },
    "message": "Withdrawal request submitted. Admin will review and process."
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> requestWithdrawal(double amount) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/wallet/withdraw'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
    body: json.encode({'amount': amount}),
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    final error = json.decode(response.body);
    throw Exception(error['message']);
  }
}

Validation:

  • Minimum: $10
  • Maximum: $10,000
  • Role: Contractors only
  • Requires sufficient balance
  • Requires Stripe Connect account setup

Offer API

Send Offer

Endpoint: POST /api/job-request/:applicationId/send-offer

Headers: Requires authentication (customer role)

Request:

{
  "amount": 100.00,
  "timeline": "2 weeks",
  "description": "Complete the project as described in the job posting"
}

Response:

{
  "status": 201,
  "message": "Offer sent successfully",
  "data": {
    "offer": {
      "_id": "507f1f77bcf86cd799439014",
      "job": "507f1f77bcf86cd799439015",
      "customer": "507f1f77bcf86cd799439011",
      "contractor": "507f1f77bcf86cd799439016",
      "application": "507f1f77bcf86cd799439017",
      "amount": 100.00,
      "platformFee": 5.00,
      "serviceFee": 20.00,
      "contractorPayout": 80.00,
      "totalCharge": 105.00,
      "timeline": "2 weeks",
      "description": "Complete the project as described in the job posting",
      "status": "pending",
      "expiresAt": "2026-02-04T12:00:00.000Z",
      "createdAt": "2026-01-28T12:00:00.000Z"
    },
    "commissions": {
      "platformFee": 5.00,
      "serviceFee": 20.00,
      "contractorPayout": 80.00,
      "totalCharge": 105.00,
      "adminCommission": 25.00
    }
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> sendOffer({
  required String applicationId,
  required double amount,
  required String timeline,
  required String description,
}) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/job-request/$applicationId/send-offer'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
    body: json.encode({
      'amount': amount,
      'timeline': timeline,
      'description': description,
    }),
  );

  if (response.statusCode == 201) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    final error = json.decode(response.body);
    throw Exception(error['message']);
  }
}

Validation:

  • Amount: $10 - $10,000
  • Timeline: 1-100 characters
  • Description: 10-1000 characters
  • Requires sufficient wallet balance
  • One offer per job only

Important: When offer is sent, wallet balance is NOT immediately deducted. Deduction happens only when contractor accepts.

Accept Offer

Endpoint: POST /api/job-request/offer/:offerId/accept

Headers: Requires authentication (contractor role)

Response:

{
  "status": 200,
  "message": "Offer accepted successfully",
  "data": {
    "offer": {
      "_id": "507f1f77bcf86cd799439014",
      "status": "accepted",
      "acceptedAt": "2026-01-28T12:30:00.000Z",
      ...
    },
    "job": {
      "_id": "507f1f77bcf86cd799439015",
      "status": "assigned",
      "contractorId": "507f1f77bcf86cd799439016",
      "offerId": "507f1f77bcf86cd799439014",
      "assignedAt": "2026-01-28T12:30:00.000Z",
      ...
    },
    "payment": {
      "customerCharged": 105.00,
      "contractorWillReceive": 80.00,
      "platformCommission": 25.00
    }
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> acceptOffer(String offerId) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/job-request/offer/$offerId/accept'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    final error = json.decode(response.body);
    throw Exception(error['message']);
  }
}

What Happens:

  1. Customer wallet: -$105 (database only, no real money transfer)
  2. Admin wallet: +$105 (database only)
  3. Job status: openassigned
  4. Contractor assigned to job
  5. Other applications automatically rejected

Reject Offer

Endpoint: POST /api/job-request/offer/:offerId/reject

Headers: Requires authentication (contractor role)

Request (optional):

{
  "reason": "Timeline is too short for this project"
}

Response:

{
  "status": 200,
  "message": "Offer rejected successfully",
  "data": {
    "offer": {
      "_id": "507f1f77bcf86cd799439014",
      "status": "rejected",
      "rejectedAt": "2026-01-28T12:30:00.000Z",
      "rejectionReason": "Timeline is too short for this project",
      ...
    },
    "refundAmount": 105.00  // If offer was previously accepted
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> rejectOffer(String offerId, {String? reason}) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/job-request/offer/$offerId/reject'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
    body: json.encode({
      if (reason != null) 'reason': reason,
    }),
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    final error = json.decode(response.body);
    throw Exception(error['message']);
  }
}

What Happens:

  1. If offer was accepted: Full refund to customer (wallet adjustment only)
  2. Application status reset to pending
  3. Job status reset to open (if was assigned)
  4. Customer can send new offer

Job API

Get Job Details

Endpoint: GET /api/job/:id

Headers: Requires authentication

Response:

{
  "status": 200,
  "message": "Job retrieved successfully",
  "data": {
    "_id": "507f1f77bcf86cd799439015",
    "title": "Build Mobile App",
    "description": "Need a Flutter mobile app...",
    "budget": 100.00,
    "status": "assigned",
    "postedBy": {...},
    "contractorId": {...},
    "offerId": "507f1f77bcf86cd799439014",
    "category": "Mobile Development",
    "location": "Remote",
    "assignedAt": "2026-01-28T12:30:00.000Z",
    "createdAt": "2026-01-28T10:00:00.000Z"
  }
}

Update Job Status

Endpoint: PATCH /api/job/:id/status

Headers: Requires authentication

Request:

{
  "status": "in_progress"
}

Valid Status Transitions:

  • openassigned, cancelled
  • assignedin_progress, cancelled
  • in_progresscompleted, cancelled

Response:

{
  "status": 200,
  "message": "Job status updated successfully",
  "data": {
    "_id": "507f1f77bcf86cd799439015",
    "status": "in_progress",
    ...
  }
}

Complete Job (Customer)

Endpoint: POST /api/job/:id/complete

Headers: Requires authentication (customer role)

Prerequisites: Job must be in in_progress status

Response:

{
  "status": 200,
  "message": "Job completion request submitted",
  "data": {
    "completionRequest": {
      "_id": "507f1f77bcf86cd799439018",
      "job": "507f1f77bcf86cd799439015",
      "requestedBy": "507f1f77bcf86cd799439011",
      "status": "pending",
      "requestedAt": "2026-01-28T14:00:00.000Z"
    },
    "message": "Admin will review and approve completion"
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> completeJob(String jobId) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/job/$jobId/complete'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    final error = json.decode(response.body);
    throw Exception(error['message']);
  }
}

What Happens:

  1. Creates completion request for admin review
  2. Admin approves in admin dashboard
  3. On approval:
    • Admin wallet: -$80 (keeps $25 commission)
    • Contractor wallet: +$80
    • Admin initiates Stripe Connect transfer to contractor
  4. Job status: completed
  5. Notifications sent to all parties

Cancel Job

Endpoint: POST /api/job/:id/cancel

Headers: Requires authentication (customer or admin)

Request (optional):

{
  "reason": "Project requirements changed"
}

Response:

{
  "status": 200,
  "message": "Job cancelled successfully",
  "data": {
    "job": {
      "_id": "507f1f77bcf86cd799439015",
      "status": "cancelled",
      "cancelledAt": "2026-01-28T14:00:00.000Z",
      "cancellationReason": "Project requirements changed",
      ...
    },
    "refundAmount": 105.00  // If offer was accepted
  }
}

What Happens:

  1. If offer was accepted: Full refund to customer (wallet adjustment only)
  2. Job status: cancelled
  3. Notification to contractor

Transaction History API

Get Transactions

Endpoint: GET /api/wallet/transactions

Headers: Requires authentication

Query Parameters:

  • page (optional): Page number (default: 1)
  • limit (optional): Items per page (default: 20, max: 100)
  • type (optional): Filter by transaction type

Example:

GET /api/wallet/transactions?page=1&limit=20&type=deposit

Response:

{
  "status": 200,
  "message": "Transactions retrieved successfully",
  "data": {
    "transactions": [
      {
        "_id": "507f1f77bcf86cd799439019",
        "type": "deposit",
        "amount": 100.00,
        "to": {
          "_id": "507f1f77bcf86cd799439011",
          "name": "John Doe",
          "email": "[email protected]"
        },
        "status": "completed",
        "description": "Deposit $100 via Stripe",
        "completedAt": "2026-01-28T10:30:00.000Z",
        "createdAt": "2026-01-28T10:29:00.000Z"
      },
      {
        "_id": "507f1f77bcf86cd79943901a",
        "type": "wallet_transfer",
        "amount": 105.00,
        "from": {
          "_id": "507f1f77bcf86cd799439011",
          "name": "John Doe"
        },
        "to": {
          "_id": "507f1f77bcf86cd799439020",
          "name": "Admin"
        },
        "offer": "507f1f77bcf86cd799439014",
        "job": "507f1f77bcf86cd799439015",
        "status": "completed",
        "description": "Offer accepted - $105 transferred to admin",
        "completedAt": "2026-01-28T12:30:00.000Z",
        "createdAt": "2026-01-28T12:30:00.000Z"
      }
    ],
    "pagination": {
      "currentPage": 1,
      "totalPages": 3,
      "totalTransactions": 45,
      "limit": 20
    }
  }
}

Flutter Implementation:

Future<Map<String, dynamic>> getTransactions({
  int page = 1,
  int limit = 20,
  String? type,
}) async {
  final queryParams = {
    'page': page.toString(),
    'limit': limit.toString(),
    if (type != null) 'type': type,
  };

  final uri = Uri.parse('$baseUrl/api/wallet/transactions')
      .replace(queryParameters: queryParams);

  final response = await http.get(
    uri,
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data['data'];
  } else {
    throw Exception('Failed to load transactions');
  }
}

Transaction Types:

  • deposit: Money added via Stripe
  • withdrawal: Money withdrawn to bank
  • wallet_transfer: Money moved between users (offer acceptance)
  • contractor_payout: Payment from admin to contractor (job completion)
  • refund: Money refunded to customer

Stripe Integration

Deposits Flow

The deposit flow uses Stripe Checkout hosted pages, which means users complete payment in their browser, not in-app.

Flutter Dependencies

# pubspec.yaml
dependencies:
  url_launcher: ^6.2.0  # For opening browser
  http: ^1.1.0          # For API calls

Complete Deposit Implementation

import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class DepositService {
  final String baseUrl;
  final String accessToken;

  DepositService({required this.baseUrl, required this.accessToken});

  Future<void> initiateDeposit(double amount) async {
    try {
      // Step 1: Request deposit from backend
      final response = await http.post(
        Uri.parse('$baseUrl/api/wallet/deposit'),
        headers: {
          'Authorization': 'Bearer $accessToken',
          'Content-Type': 'application/json',
        },
        body: json.encode({'amount': amount}),
      );

      if (response.statusCode == 200) {
        final data = json.decode(response.body)['data'];
        final checkoutUrl = data['checkoutUrl'];
        
        // Step 2: Open Stripe Checkout in browser
        await _openCheckout(checkoutUrl);
        
        // Step 3: Wait for user to return
        // You can show a waiting screen here
      } else {
        throw Exception('Failed to create deposit');
      }
    } catch (e) {
      throw Exception('Deposit error: $e');
    }
  }

  Future<void> _openCheckout(String url) async {
    final Uri uri = Uri.parse(url);
    
    // Opens in external browser (NOT in-app)
    if (await canLaunchUrl(uri)) {
      await launchUrl(
        uri,
        mode: LaunchMode.externalApplication,  // IMPORTANT: Use external browser
      );
    } else {
      throw Exception('Could not launch checkout URL');
    }
  }
}

Checking Payment Completion

After user returns from browser, check if payment was completed:

Option 1: Polling Wallet Balance

Future<void> waitForDepositCompletion() async {
  // Poll every 3 seconds for up to 2 minutes
  for (int i = 0; i < 40; i++) {
    await Future.delayed(Duration(seconds: 3));
    
    final wallet = await getWallet();
    // Check if balance increased
    // Store previous balance before initiating deposit for comparison
  }
}

Option 2: Push Notifications (Recommended)

// Backend sends push notification when webhook confirms payment
// Listen for notification in Flutter
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  if (message.data['type'] == 'deposit_completed') {
    // Refresh wallet balance
    // Update UI
  }
});

Stripe Connect (Contractors)

Contractors need to set up Stripe Connect accounts to receive payouts:

Onboarding Link Endpoint: POST /api/wallet/stripe/onboard

Future<String> getStripeOnboardingLink() async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/wallet/stripe/onboard'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = json.decode(response.body)['data'];
    return data['onboardingUrl'];
  } else {
    throw Exception('Failed to get onboarding link');
  }
}

// Usage
final onboardingUrl = await getStripeOnboardingLink();
await launchUrl(
  Uri.parse(onboardingUrl),
  mode: LaunchMode.externalApplication,
);

Check Onboarding Status: GET /api/wallet/stripe/status

Future<Map<String, dynamic>> getStripeStatus() async {
  final response = await http.get(
    Uri.parse('$baseUrl/api/wallet/stripe/status'),
    headers: {
      'Authorization': 'Bearer $accessToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return json.decode(response.body)['data'];
    // Returns: { "isConnected": true/false, "canReceivePayouts": true/false }
  } else {
    throw Exception('Failed to get Stripe status');
  }
}

Error Handling

Error Response Structure

{
  "status": 400,
  "message": "Insufficient balance",
  "data": null,
  "errors": [
    {
      "field": "amount",
      "message": "Required: $105, Available: $50"
    }
  ]
}

Common Error Codes

Status Meaning Example
400 Bad Request Invalid input, validation failed
401 Unauthorized Missing or invalid token
403 Forbidden Insufficient permissions
404 Not Found Resource doesn't exist
500 Server Error Internal server error

Flutter Error Handling

Future<Map<String, dynamic>> apiCall() async {
  try {
    final response = await http.post(...);
    
    final data = json.decode(response.body);
    
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return data['data'];
    } else {
      // Handle error response
      throw ApiException(
        statusCode: response.statusCode,
        message: data['message'] ?? 'Unknown error',
        errors: data['errors'],
      );
    }
  } on SocketException {
    throw NetworkException('No internet connection');
  } on FormatException {
    throw ParseException('Invalid response format');
  } catch (e) {
    throw Exception('Unexpected error: $e');
  }
}

// Custom exception classes
class ApiException implements Exception {
  final int statusCode;
  final String message;
  final List<dynamic>? errors;

  ApiException({
    required this.statusCode,
    required this.message,
    this.errors,
  });
}

Specific Error Scenarios

Insufficient Balance:

try {
  await sendOffer(...);
} on ApiException catch (e) {
  if (e.statusCode == 400 && e.message.contains('Insufficient balance')) {
    // Show deposit dialog
    showDepositDialog();
  }
}

Wallet Frozen:

try {
  await depositMoney(100);
} on ApiException catch (e) {
  if (e.message.contains('frozen')) {
    // Show contact support dialog
    showContactSupportDialog();
  }
}

Best Practices

1. Token Management

// Store tokens securely
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStorage {
  final storage = FlutterSecureStorage();

  Future<void> saveTokens(String accessToken, String refreshToken) async {
    await storage.write(key: 'access_token', value: accessToken);
    await storage.write(key: 'refresh_token', value: refreshToken);
  }

  Future<String?> getAccessToken() async {
    return await storage.read(key: 'access_token');
  }
}

2. API Service Layer

// Centralized API service
class ApiService {
  final String baseUrl;
  final TokenStorage tokenStorage;

  ApiService({required this.baseUrl, required this.tokenStorage});

  Future<Map<String, String>> _getHeaders() async {
    final token = await tokenStorage.getAccessToken();
    return {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    };
  }

  Future<http.Response> get(String endpoint) async {
    return http.get(
      Uri.parse('$baseUrl$endpoint'),
      headers: await _getHeaders(),
    );
  }

  Future<http.Response> post(String endpoint, Map<String, dynamic> body) async {
    return http.post(
      Uri.parse('$baseUrl$endpoint'),
      headers: await _getHeaders(),
      body: json.encode(body),
    );
  }
}

3. State Management

// Using Provider for wallet state
import 'package:flutter/foundation.dart';

class WalletProvider with ChangeNotifier {
  double _balance = 0.0;
  bool _isLoading = false;

  double get balance => _balance;
  bool get isLoading => _isLoading;

  Future<void> loadWallet(WalletService service) async {
    _isLoading = true;
    notifyListeners();

    try {
      final wallet = await service.getWallet();
      _balance = wallet['balance'];
    } catch (e) {
      // Handle error
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  Future<void> refreshBalance(WalletService service) async {
    final wallet = await service.getWallet();
    _balance = wallet['balance'];
    notifyListeners();
  }
}

4. Loading States

// Show loading indicator during API calls
class PaymentScreen extends StatefulWidget {
  @override
  _PaymentScreenState createState() => _PaymentScreenState();
}

class _PaymentScreenState extends State<PaymentScreen> {
  bool _isProcessing = false;

  Future<void> _sendOffer() async {
    setState(() {
      _isProcessing = true;
    });

    try {
      await offerService.sendOffer(...);
      // Show success
    } catch (e) {
      // Show error
    } finally {
      setState(() {
        _isProcessing = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isProcessing ? null : _sendOffer,
      child: _isProcessing
          ? CircularProgressIndicator()
          : Text('Send Offer'),
    );
  }
}

5. Validation Before API Calls

Future<void> sendOffer({
  required double amount,
  required String timeline,
  required String description,
}) async {
  // Client-side validation
  if (amount < 10 || amount > 10000) {
    throw Exception('Amount must be between \$10 and \$10,000');
  }

  if (timeline.length < 1 || timeline.length > 100) {
    throw Exception('Timeline must be 1-100 characters');
  }

  if (description.length < 10 || description.length > 1000) {
    throw Exception('Description must be 10-1000 characters');
  }

  // Check balance
  final wallet = await getWallet();
  final totalCharge = amount * 1.05;  // Include 5% platform fee
  
  if (wallet['balance'] < totalCharge) {
    throw Exception('Insufficient balance');
  }

  // Make API call
  await offerService.sendOffer(...);
}

6. Refresh on App Resume

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // User returned from browser after Stripe checkout
      // Refresh wallet balance
      walletProvider.refreshBalance(walletService);
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(...);
  }
}

Support

For API integration questions:

  • Backend API documentation: doc/payment/1.MAIN-REFERENCE.md
  • Backend implementation: doc/payment/2.BACKEND_IMPLEMENTATION.md
  • Technical support: [email protected]

Document Version: 2.0.0
Last Updated: January 28, 2026
Status: ✅ Complete and Production Ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment