Skip to content

Instantly share code, notes, and snippets.

@yeasin2002
Last active January 25, 2026 10:23
Show Gist options
  • Select an option

  • Save yeasin2002/7370eb6552859fcaf3a78bf85843b999 to your computer and use it in GitHub Desktop.

Select an option

Save yeasin2002/7370eb6552859fcaf3a78bf85843b999 to your computer and use it in GitHub Desktop.
Frontend API Guide (Flutter Mobile App) with Stripe Payment

3. Frontend API Guide (Flutter Mobile App)

Version: 2.0.0
Last Updated: January 25, 2026
For: Flutter Mobile Developers
Status: ✅ Stripe Integration Complete


Quick Reference

Base URL

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

Authentication

Authorization: Bearer <access_token>
Content-Type: application/json

Commission Structure

$100 Job Example:
├── Customer Pays: $105 (100 + 5%)
├── Platform Fee: $5 → Admin (on accept)
├── Service Fee: $20 → Admin (on complete)
└── Contractor Gets: $80 (on complete)

Table of Contents

  1. Stripe Setup for Flutter
  2. Wallet Endpoints
  3. Stripe Connect Onboarding
  4. Offer Endpoints
  5. Job Endpoints
  6. Data Models
  7. Error Handling
  8. Testing Guide

Stripe Setup for Flutter

Dependencies

Add to pubspec.yaml:

dependencies:
  flutter_stripe: ^10.1.1  # Latest stable version
  http: ^1.1.0
  flutter_secure_storage: ^9.0.0

Initialize Stripe

// lib/core/stripe_config.dart
import 'package:flutter_stripe/flutter_stripe.dart';

class StripeConfig {
  static const String publishableKey = 'pk_test_...'; // From backend
  
  static Future<void> initialize() async {
    Stripe.publishableKey = publishableKey;
    await Stripe.instance.applySettings();
  }
}

// Call in main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await StripeConfig.initialize();
  runApp(MyApp());
}

Get Publishable Key from Backend

// The publishable key can be fetched from backend or hardcoded
// GET /api/wallet/stripe-config (optional endpoint)
// Returns: { "publishableKey": "pk_test_..." }

Wallet Endpoints

GET /wallet

Get user wallet balance.

Authentication: Required

Response:

{
  "status": 200,
  "message": "Wallet retrieved successfully",
  "data": {
    "_id": "wallet_id",
    "user": "user_id",
    "balance": 1000,
    "escrowBalance": 105,
    "pendingDeposits": 0,
    "currency": "USD",
    "totalEarnings": 5000,
    "totalSpent": 2000,
    "isActive": true,
    "isFrozen": false
  }
}

Flutter Implementation:

Future<Wallet> getWallet() async {
  final response = await http.get(
    Uri.parse('$baseUrl/wallet'),
    headers: await _getHeaders(),
  );
  
  if (response.statusCode == 200) {
    return Wallet.fromJson(json.decode(response.body)['data']);
  }
  throw WalletException('Failed to load wallet');
}

POST /wallet/deposit

Add money to wallet using Stripe Payment Intent.

Authentication: Required
Role: Any registered user

Request Body:

{
  "amount": 100,
  "paymentMethodId": "pm_xxx..."
}

Validation:

  • amount: Minimum $10, positive number
  • paymentMethodId: Stripe payment method ID (obtained from Stripe SDK)

How to Get paymentMethodId

The paymentMethodId is obtained from the Stripe SDK on the client side. Here's how to get it:

Flutter (using flutter_stripe package):

  1. Install the package:

    # pubspec.yaml
    dependencies:
      flutter_stripe: ^10.0.0
  2. Initialize Stripe (in your app startup):

    import 'package:flutter_stripe/flutter_stripe.dart';
    
    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      
      // Set your publishable key
      Stripe.publishableKey = 'pk_test_...'; // Your Stripe publishable key
      
      runApp(MyApp());
    }
  3. Create Payment Method (when user enters card details):

    import 'package:flutter_stripe/flutter_stripe.dart';
    
    Future<String?> createPaymentMethod() async {
      try {
        // Show card input form
        await Stripe.instance.presentPaymentSheet();
        
        // Or create payment method from card details
        final paymentMethod = await Stripe.instance.createPaymentMethod(
          params: PaymentMethodParams.card(
            paymentMethodData: PaymentMethodData(
              billingDetails: BillingDetails(
                email: '[email protected]',
                name: 'John Doe',
              ),
            ),
          ),
        );
        
        // Return the payment method ID
        return paymentMethod.id; // This is your paymentMethodId (pm_xxx...)
      } catch (e) {
        print('Error creating payment method: $e');
        return null;
      }
    }
  4. Use CardField widget (for custom UI):

    import 'package:flutter_stripe/flutter_stripe.dart';
    
    class PaymentScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Column(
            children: [
              // Card input field
              CardField(
                onCardChanged: (card) {
                  print('Card changed: ${card?.complete}');
                },
              ),
              
              ElevatedButton(
                onPressed: () async {
                  // Create payment method
                  final paymentMethod = await Stripe.instance.createPaymentMethod(
                    params: PaymentMethodParams.card(
                      paymentMethodData: PaymentMethodData(),
                    ),
                  );
                  
                  // Send to your backend
                  final paymentMethodId = paymentMethod.id;
                  await depositToWallet(amount: 100, paymentMethodId: paymentMethodId);
                },
                child: Text('Deposit'),
              ),
            ],
          ),
        );
      }
    }

React/Web (using @stripe/stripe-js and @stripe/react-stripe-js):

  1. Install packages:

    npm install @stripe/stripe-js @stripe/react-stripe-js
  2. Setup Stripe Elements:

    import { loadStripe } from '@stripe/stripe-js';
    import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
    
    // Initialize Stripe
    const stripePromise = loadStripe('pk_test_...'); // Your publishable key
    
    function App() {
      return (
        <Elements stripe={stripePromise}>
          <DepositForm />
        </Elements>
      );
    }
  3. Create Payment Method:

    function DepositForm() {
      const stripe = useStripe();
      const elements = useElements();
      
      const handleDeposit = async (amount) => {
        if (!stripe || !elements) return;
        
        // Get card element
        const cardElement = elements.getElement(CardElement);
        
        // Create payment method
        const { error, paymentMethod } = await stripe.createPaymentMethod({
          type: 'card',
          card: cardElement,
          billing_details: {
            name: 'John Doe',
            email: '[email protected]',
          },
        });
        
        if (error) {
          console.error('Error:', error);
          return;
        }
        
        // Send to your backend
        const paymentMethodId = paymentMethod.id; // pm_xxx...
        await depositToWallet(amount, paymentMethodId);
      };
      
      return (
        <form onSubmit={(e) => { e.preventDefault(); handleDeposit(100); }}>
          <CardElement />
          <button type="submit">Deposit $100</button>
        </form>
      );
    }

Important Notes:

  • Never send raw card details to your backend
  • Always use Stripe SDK to tokenize card information
  • The paymentMethodId (pm_xxx...) is safe to send to your backend
  • Use your Stripe publishable key (pk_test_... or pk_live_...) on the client
  • Keep your Stripe secret key (sk_test_... or sk_live_...) on the backend only

Response (Success):

{
  "status": 200,
  "message": "Payment initiated successfully",
  "data": {
    "paymentIntent": {
      "id": "pi_xxx...",
      "status": "succeeded",
      "amount": 100,
      "clientSecret": "pi_xxx_secret_xxx"
    },
    "transaction": {
      "_id": "txn_123",
      "amount": 100,
      "type": "deposit",
      "status": "pending",
      "stripePaymentIntentId": "pi_xxx..."
    },
    "wallet": {
      "balance": 900,
      "pendingDeposits": 100
    }
  }
}

Response (Requires 3D Secure):

{
  "status": 200,
  "message": "Additional authentication required",
  "data": {
    "requiresAction": true,
    "clientSecret": "pi_xxx_secret_xxx",
    "paymentIntentId": "pi_xxx..."
  }
}

Flutter Implementation (Complete Flow):

import 'package:flutter_stripe/flutter_stripe.dart';

class DepositService {
  /// Complete deposit flow with Stripe
  Future<DepositResult> makeDeposit(double amount) async {
    try {
      // Step 1: Create payment method from card
      final paymentMethod = await Stripe.instance.createPaymentMethod(
        params: PaymentMethodParams.card(
          paymentMethodData: PaymentMethodData(
            billingDetails: BillingDetails(
              email: userEmail,
              name: userName,
            ),
          ),
        ),
      );

      // Step 2: Call backend to create payment intent
      final response = await http.post(
        Uri.parse('$baseUrl/wallet/deposit'),
        headers: await _getHeaders(),
        body: json.encode({
          'amount': amount,
          'paymentMethodId': paymentMethod.id,
        }),
      );

      final data = json.decode(response.body);

      if (response.statusCode != 200) {
        throw DepositException(data['message']);
      }

      // Step 3: Handle 3D Secure if required
      if (data['data']['requiresAction'] == true) {
        final clientSecret = data['data']['clientSecret'];
        
        // This will show the 3DS authentication sheet
        await Stripe.instance.handleNextAction(clientSecret);
        
        // After 3DS, webhook will update the balance
        return DepositResult(
          status: 'processing',
          message: 'Payment is being processed',
        );
      }

      // Step 4: Payment succeeded immediately
      return DepositResult(
        status: 'succeeded',
        amount: amount,
        newBalance: data['data']['wallet']['balance'],
      );

    } on StripeException catch (e) {
      throw DepositException(e.error.localizedMessage ?? 'Payment failed');
    }
  }
}

Errors:

Error Message
400 "Minimum deposit amount is $10"
400 "Your card was declined"
400 "Your card has insufficient funds"
400 "Your card has expired"
429 "Rate limit exceeded. Try again later" (5 requests/hour)

POST /wallet/withdraw

Withdraw money from wallet (contractors only).

Authentication: Required
Role: Contractor (with verified Stripe Connect account)

Prerequisites:

  • User must have completed Stripe Connect onboarding
  • Account status must be "verified"
  • charges_enabled and payouts_enabled must be true

Request Body:

{
  "amount": 50
}

Validation:

  • amount: Minimum $10, maximum $10,000
  • Must have sufficient balance
  • Wallet must not be frozen
  • Stripe Connect account must be verified

Response:

{
  "status": 200,
  "message": "Withdrawal initiated successfully",
  "data": {
    "transfer": {
      "id": "tr_xxx...",
      "amount": 50,
      "status": "pending"
    },
    "transaction": {
      "_id": "txn_123",
      "amount": 50,
      "type": "withdrawal",
      "status": "pending",
      "stripeTransferId": "tr_xxx..."
    },
    "wallet": {
      "balance": 30,
      "totalWithdrawals": 50
    },
    "estimatedArrival": "1-2 business days"
  }
}

Flutter Implementation:

Future<WithdrawalResult> requestWithdrawal(double amount) async {
  final response = await http.post(
    Uri.parse('$baseUrl/wallet/withdraw'),
    headers: await _getHeaders(),
    body: json.encode({'amount': amount}),
  );

  final data = json.decode(response.body);

  if (response.statusCode == 200) {
    return WithdrawalResult.fromJson(data['data']);
  }

  throw WithdrawalException(data['message']);
}

Errors:

Error Message
400 "Only contractors can withdraw funds"
400 "Insufficient balance. Available: $X"
400 "Wallet is frozen. Please contact support"
400 "Please complete Stripe Connect onboarding"
400 "Stripe Connect account is pending verification"
400 "Your Stripe account is not fully activated"
429 "Rate limit exceeded. Try again later" (3 requests/hour)

GET /wallet/transactions

Get transaction history with pagination.

Authentication: Required

Query Parameters:

Parameter Type Default Description
page number 1 Page number
limit number 10 Items per page (max 50)
type string - Filter by transaction type

Transaction Types:

  • deposit, withdrawal, escrow_hold, escrow_release
  • platform_fee, service_fee, contractor_payout, refund

Response:

{
  "status": 200,
  "message": "Transactions retrieved successfully",
  "data": {
    "transactions": [
      {
        "_id": "txn_123",
        "type": "deposit",
        "amount": 100,
        "from": { "_id": "user_id", "full_name": "John Doe" },
        "to": { "_id": "user_id", "full_name": "John Doe" },
        "status": "completed",
        "stripePaymentIntentId": "pi_xxx...",
        "description": "Wallet deposit of 100",
        "createdAt": "2026-01-24T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 50,
      "totalPages": 5
    }
  }
}

GET /wallet/withdrawal/:transactionId/status

Check withdrawal status.

Authentication: Required
Role: Contractor (owner of the transaction)

Response:

{
  "status": 200,
  "message": "Withdrawal status retrieved",
  "data": {
    "transaction": {
      "_id": "txn_123",
      "amount": 50,
      "status": "completed",
      "stripeTransferId": "tr_xxx..."
    },
    "stripeTransfer": {
      "id": "tr_xxx...",
      "amount": 5000,
      "currency": "usd",
      "created": 1706185200,
      "reversed": false
    }
  }
}

Stripe Connect Onboarding

Contractors must complete Stripe Connect onboarding to receive payouts.

POST /wallet/connect-account

Create Stripe Connect Express account.

Authentication: Required
Role: Contractor

Response:

{
  "status": 201,
  "message": "Stripe Connect account created. Please complete onboarding.",
  "data": {
    "accountId": "acct_xxx...",
    "onboardingUrl": "https://connect.stripe.com/express/onboarding/...",
    "expiresAt": "2026-01-25T11:00:00Z"
  }
}

Flutter Implementation:

import 'package:url_launcher/url_launcher.dart';

Future<void> startStripeOnboarding() async {
  final response = await http.post(
    Uri.parse('$baseUrl/wallet/connect-account'),
    headers: await _getHeaders(),
  );

  if (response.statusCode == 201) {
    final data = json.decode(response.body)['data'];
    final onboardingUrl = data['onboardingUrl'];
    
    // Open Stripe onboarding in browser
    if (await canLaunchUrl(Uri.parse(onboardingUrl))) {
      await launchUrl(
        Uri.parse(onboardingUrl),
        mode: LaunchMode.externalApplication,
      );
    }
  }
}

Errors:

Error Message
400 "Only contractors can create Connect accounts"
400 "Stripe Connect account already exists"
429 "Rate limit exceeded. Try again later" (2 requests/hour)

GET /wallet/connect-account/status

Get Stripe Connect account status.

Authentication: Required
Role: Contractor

Response (No Account):

{
  "status": 200,
  "message": "No Stripe Connect account found",
  "data": {
    "hasAccount": false
  }
}

Response (Account Exists):

{
  "status": 200,
  "message": "Stripe Connect account status retrieved",
  "data": {
    "hasAccount": true,
    "accountId": "acct_xxx...",
    "status": "verified",
    "onboardingComplete": true,
    "chargesEnabled": true,
    "payoutsEnabled": true,
    "requirements": {
      "currentlyDue": [],
      "pendingVerification": [],
      "errors": []
    },
    "capabilities": {
      "card_payments": "active",
      "transfers": "active"
    }
  }
}

Account Status Values:

Status Description Can Withdraw?
pending Onboarding incomplete ❌ No
verified Fully verified ✅ Yes
rejected Verification failed ❌ No

Flutter Implementation:

Future<ConnectAccountStatus> getConnectAccountStatus() async {
  final response = await http.get(
    Uri.parse('$baseUrl/wallet/connect-account/status'),
    headers: await _getHeaders(),
  );

  final data = json.decode(response.body)['data'];
  return ConnectAccountStatus.fromJson(data);
}

// UI Logic
Widget buildPayoutSection() {
  return FutureBuilder<ConnectAccountStatus>(
    future: getConnectAccountStatus(),
    builder: (context, snapshot) {
      if (snapshot.data?.onboardingComplete != true) {
        return ElevatedButton(
          onPressed: startStripeOnboarding,
          child: Text('Complete Payout Setup'),
        );
      }
      
      return ElevatedButton(
        onPressed: () => showWithdrawalDialog(),
        child: Text('Withdraw Funds'),
      );
    },
  );
}

GET /wallet/connect-account/refresh

Get a new onboarding link if the previous one expired.

Authentication: Required
Role: Contractor with existing Connect account

Response:

{
  "status": 200,
  "message": "New onboarding link generated",
  "data": {
    "onboardingUrl": "https://connect.stripe.com/express/onboarding/...",
    "expiresAt": "2026-01-25T11:00:00Z"
  }
}

Offer Endpoints

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

Customer sends offer to contractor.

Authentication: Required
Role: Customer

Path Parameters:

  • applicationId: Job application ID

Request Body:

{
  "amount": 100,
  "timeline": "7 days",
  "description": "Complete plumbing repair as discussed"
}

Validation:

  • amount: Min $10, Max $10,000
  • timeline: Min 1 char, Max 100 chars
  • description: Min 10 chars, Max 1000 chars

Response:

{
  "status": 201,
  "message": "Offer sent successfully",
  "data": {
    "offer": {
      "_id": "offer_123",
      "job": "job_123",
      "customer": "customer_123",
      "contractor": "contractor_123",
      "amount": 100,
      "platformFee": 5,
      "serviceFee": 20,
      "contractorPayout": 80,
      "totalCharge": 105,
      "timeline": "7 days",
      "status": "pending",
      "expiresAt": "2026-01-31T10:00:00Z"
    },
    "walletBalance": 895,
    "amounts": {
      "jobBudget": 100,
      "platformFee": 5,
      "serviceFee": 20,
      "contractorPayout": 80,
      "totalCharge": 105,
      "adminTotal": 25
    }
  }
}

What Happens:

  1. $105 (job + 5% platform fee) held in escrow
  2. Offer created with status "pending"
  3. Contractor notified via push notification
  4. Offer expires in 7 days if not responded

Errors:

  • 400: "Insufficient balance. Required: $X, Available: $Y"
  • 400: "Job is not open for offers"
  • 400: "An offer already exists for this job"
  • 403: "Not authorized"

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

Contractor accepts offer.

Authentication: Required
Role: Contractor

Response:

{
  "status": 200,
  "message": "Offer accepted successfully",
  "data": {
    "offer": {
      "_id": "offer_123",
      "status": "accepted",
      "acceptedAt": "2026-01-24T11:00:00Z"
    },
    "job": {
      "_id": "job_123",
      "status": "assigned",
      "contractorId": "contractor_123"
    },
    "payment": {
      "platformFee": 5,
      "serviceFee": 20,
      "contractorPayout": 80
    }
  }
}

What Happens:

  1. Offer status → "accepted"
  2. Job status → "assigned"
  3. Platform fee ($5) → Admin wallet
  4. Remaining $100 stays in escrow
  5. Other applications rejected

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

Contractor rejects offer.

Authentication: Required
Role: Contractor

Request Body:

{
  "reason": "Timeline too short for quality work"
}

Response:

{
  "status": 200,
  "message": "Offer rejected successfully",
  "data": {
    "offer": {
      "_id": "offer_123",
      "status": "rejected",
      "rejectedAt": "2026-01-24T11:00:00Z",
      "rejectionReason": "Timeline too short"
    },
    "refundAmount": 105
  }
}

What Happens:

  1. Offer status → "rejected"
  2. Full refund ($105) → Customer wallet
  3. Customer can send new offer

Job Endpoints

POST /job/:id/complete

Mark job as complete (customer only).

Authentication: Required
Role: Customer

Prerequisites:

  • Job must be "in_progress"
  • Customer must own the job
  • Offer must be "accepted"

Response:

{
  "status": 200,
  "message": "Job marked as complete",
  "data": {
    "job": {
      "_id": "job_123",
      "status": "completed",
      "completedAt": "2026-01-24T15:00:00Z"
    },
    "payment": {
      "serviceFee": 20,
      "contractorPayout": 80,
      "adminCommission": 25
    }
  }
}

What Happens:

  1. Job status → "completed"
  2. Service fee ($20) → Admin wallet
  3. Contractor payout ($80) → Contractor wallet
  4. Escrow released

POST /job/:id/cancel

Cancel job.

Authentication: Required
Role: Customer or Admin

Request Body:

{
  "reason": "No longer needed"
}

Response:

{
  "status": 200,
  "message": "Job cancelled successfully",
  "data": {
    "job": {
      "_id": "job_123",
      "status": "cancelled"
    },
    "refundAmount": 105
  }
}

Data Models

Wallet

class Wallet {
  final String id;
  final String user;
  final double balance;           // Available funds
  final double escrowBalance;     // Funds in escrow
  final double pendingDeposits;   // Stripe deposits processing
  final String currency;          // "USD"
  final bool isActive;
  final bool isFrozen;
  final double totalEarnings;     // Lifetime earnings
  final double totalSpent;        // Lifetime spending
  final double totalWithdrawals;  // Lifetime withdrawals
  final DateTime createdAt;
  final DateTime updatedAt;
}

Transaction

class Transaction {
  final String id;
  final String type;              // deposit, withdrawal, etc.
  final double amount;
  final User from;
  final User to;
  final String? offerId;
  final String? jobId;
  final String status;            // pending, completed, failed
  final String? stripePaymentIntentId;
  final String? stripeTransferId;
  final String description;
  final String? failureReason;
  final DateTime createdAt;
}

ConnectAccountStatus

class ConnectAccountStatus {
  final bool hasAccount;
  final String? accountId;
  final String status;            // pending, verified, rejected
  final bool onboardingComplete;
  final bool chargesEnabled;
  final bool payoutsEnabled;
  final List<String> currentlyDue;
  final List<String> pendingVerification;
}

Error Handling

Standard Error Response

{
  "status": 400,
  "message": "Error description",
  "data": null,
  "errors": [
    {
      "field": "amount",
      "message": "Amount must be positive"
    }
  ]
}

Stripe-Specific Errors

Error Code User Message
Card declined card_declined "Your card was declined"
Insufficient funds insufficient_funds "Your card has insufficient funds"
Expired card expired_card "Your card has expired"
Incorrect CVC incorrect_cvc "Your card's security code is incorrect"
Processing error processing_error "An error occurred. Please try again"
Rate limited rate_limit "Too many attempts. Try again later"

Flutter Error Handling

class ApiException implements Exception {
  final String message;
  final int statusCode;
  final String? field;
  
  ApiException(this.message, this.statusCode, [this.field]);
}

Future<T> handleApiResponse<T>(
  http.Response response,
  T Function(Map<String, dynamic>) parser,
) {
  final data = json.decode(response.body);
  
  if (response.statusCode >= 200 && response.statusCode < 300) {
    return parser(data['data']);
  }
  
  throw ApiException(
    data['message'] ?? 'An error occurred',
    response.statusCode,
  );
}

Testing Guide

Stripe Test Cards

Card Number Scenario
4242 4242 4242 4242 ✅ Success
4000 0000 0000 0002 ❌ Declined (insufficient funds)
4000 0000 0000 0341 ❌ Declined (generic)
4000 0000 0000 9995 ❌ Declined (insufficient funds)
4000 0000 0000 0069 ❌ Expired card
4000 0000 0000 0127 ❌ Incorrect CVC
4000 0027 6000 3184 🔐 Requires 3D Secure

Test Expiry & CVC

  • Expiry: Any future date (e.g., 12/30)
  • CVC: Any 3 digits (e.g., 123)
  • ZIP: Any 5 digits (e.g., 12345)

Testing Flow

  1. Development: Use pk_test_... and sk_test_... keys
  2. Use test cards listed above
  3. Backend webhooks process automatically via Stripe CLI:
    stripe listen --forward-to localhost:4000/api/webhooks/stripe

Sample Test User

// Customer login
final loginResponse = await http.post(
  Uri.parse('$baseUrl/auth/login'),
  headers: {'Content-Type': 'application/json'},
  body: json.encode({
    'email': '[email protected]',
    'password': 'password123',
  }),
);

Rate Limits

Endpoint Limit Window
POST /wallet/deposit 5 requests 1 hour
POST /wallet/withdraw 3 requests 1 hour
POST /wallet/connect-account 2 requests 1 hour

Response when rate limited:

{
  "status": 429,
  "message": "Too many requests. Please try again in X minutes."
}

Best Practices

1. Secure Token Storage

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

Future<void> saveToken(String token) async {
  await storage.write(key: 'access_token', value: token);
}

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

2. Pull-to-Refresh for Wallet

RefreshIndicator(
  onRefresh: () => getWallet(),
  child: WalletView(),
)

3. Show Pending States

if (wallet.pendingDeposits > 0) {
  return Text('+ \$${wallet.pendingDeposits} pending');
}

4. Handle 3DS Authentication

// Always check for requires_action in deposit response
if (response.data['requiresAction'] == true) {
  await Stripe.instance.handleNextAction(clientSecret);
}

Support Resources


Related Documentation:

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