When confirming PaymentIntent in Firebase Functions

payment_intent = stripe.PaymentIntent.confirm( payment_intent_id, payment_method=payment_method_id, return_url=“https://your-region-your-project.cloudfunctions.net/payment_return" )

On Server

import stripe
import json
from firebase_functions import https_fn, options
from firebase_admin import initialize_app, firestore

initialize_app()
stripe.api_key = "sk_test_..."

@https_fn.on_request(cors=options.CorsOptions(
    cors_origins=["*"],
    cors_methods=["POST", "GET"]
))
def create_payment_intent(req: https_fn.Request) -> https_fn.Response:
    """Create PaymentIntent for redirect-based payments"""
    try:
        data = req.get_json()
        
        # Create PaymentIntent with return URL
        payment_intent = stripe.PaymentIntent.create(
            amount=int(data['amount'] * 100),  # Convert to cents
            currency=data['currency'],
            payment_method_types=[data['payment_method_type']],  # e.g., 'ideal', 'bancontact'
            metadata={
                'user_id': data.get('user_id'),
                'order_id': data.get('order_id')
            }
        )
        
        return https_fn.Response(
            json.dumps({
                'client_secret': payment_intent.client_secret,
                'payment_intent_id': payment_intent.id
            }),
            headers={'Content-Type': 'application/json'}
        )
        
    except Exception as e:
        return https_fn.Response(
            json.dumps({'error': str(e)}),
            status=400,
            headers={'Content-Type': 'application/json'}
        )

@https_fn.on_request(cors=options.CorsOptions(
    cors_origins=["*"],
    cors_methods=["POST"]
))
def confirm_payment(req: https_fn.Request) -> https_fn.Response:
    """Confirm payment with redirect handling"""
    try:
        data = req.get_json()
        
        # Confirm the PaymentIntent
        payment_intent = stripe.PaymentIntent.confirm(
            data['payment_intent_id'],
            payment_method=data['payment_method_id'],
            return_url=data['return_url']  # Your app's deep link
        )
        
        return https_fn.Response(
            json.dumps({
                'status': payment_intent.status,
                'next_action': payment_intent.next_action,
                'client_secret': payment_intent.client_secret
            }),
            headers={'Content-Type': 'application/json'}
        )
        
    except Exception as e:
        return https_fn.Response(
            json.dumps({'error': str(e)}),
            status=400,
            headers={'Content-Type': 'application/json'}
        )

@https_fn.on_request()
def payment_webhook(req: https_fn.Request) -> https_fn.Response:
    """Handle Stripe webhooks"""
    payload = req.data
    sig_header = req.headers.get('stripe-signature')
    endpoint_secret = "whsec_..."  # Your webhook secret
    
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError:
        return https_fn.Response("Invalid payload", status=400)
    except stripe.error.SignatureVerificationError:
        return https_fn.Response("Invalid signature", status=400)
    
    # Handle payment events
    if event['type'] == 'payment_intent.succeeded':
        payment_intent = event['data']['object']
        # Update Firestore with payment success
        update_payment_status(payment_intent['id'], 'succeeded')
        
    elif event['type'] == 'payment_intent.payment_failed':
        payment_intent = event['data']['object']
        update_payment_status(payment_intent['id'], 'failed')
    
    return https_fn.Response("Success")

def update_payment_status(payment_intent_id: str, status: str):
    """Update payment status in Firestore"""
    db = firestore.client()
    db.collection('payments').document(payment_intent_id).set({
        'status': status,
        'updated_at': firestore.SERVER_TIMESTAMP
    }, merge=True)

On Client (Flutter)

// lib/services/payment_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class PaymentService {
  final String baseUrl = 'https://your-region-your-project.cloudfunctions.net';
  
  Future<Map<String, dynamic>> createPaymentIntent({
    required double amount,
    required String currency,
    required String paymentMethodType,
    String? userId,
    String? orderId,
  }) async {
    
    final response = await http.post(
      Uri.parse('$baseUrl/create_payment_intent'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'amount': amount,
        'currency': currency,
        'payment_method_type': paymentMethodType,
        'user_id': userId,
        'order_id': orderId,
      }),
    );
    
    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else {
      throw Exception('Failed to create payment intent');
    }
  }
  
  Future<void> processRedirectPayment({
    required String paymentMethodType,
    required double amount,
    required String currency,
  }) async {
    
    try {
      // 1. Create PaymentIntent
      final paymentData = await createPaymentIntent(
        amount: amount,
        currency: currency,
        paymentMethodType: paymentMethodType,
      );
      
      // 2. Create payment method
      final paymentMethod = await _createPaymentMethod(paymentMethodType);
      
      // 3. Confirm payment with return URL
      final confirmResult = await http.post(
        Uri.parse('$baseUrl/confirm_payment'),
        headers: {'Content-Type': 'application/json'},
        body: json.encode({
          'payment_intent_id': paymentData['payment_intent_id'],
          'payment_method_id': paymentMethod['id'],
          'return_url': '$baseUrl/payment_return', // Firebase function URL
        }),
      );
      
      if (confirmResult.statusCode == 200) {
        final result = json.decode(confirmResult.body);
        
        if (result['next_action'] != null) {
          // Handle redirect
          await _handleRedirect(result['next_action'], paymentData['payment_intent_id']);
        }
      }
      
    } catch (e) {
      throw Exception('Payment failed: $e');
    }
  }
  
  Future<Map<String, dynamic>> _createPaymentMethod(String type) async {
    // Create payment method based on type
    switch (type) {
      case 'ideal':
        return {'id': 'pm_ideal_test', 'type': 'ideal'};
      case 'bancontact':
        return {'id': 'pm_bancontact_test', 'type': 'bancontact'};
      default:
        throw Exception('Unsupported payment method');
    }
  }
  
  Future<void> _handleRedirect(Map<String, dynamic> nextAction, String paymentIntentId) async {
    if (nextAction['type'] == 'redirect_to_url') {
      final redirectUrl = nextAction['redirect_to_url']['url'];
      
      // Launch external browser for payment
      if (await canLaunch(redirectUrl)) {
        await launch(redirectUrl, forceSafariVC: false, forceWebView: false);
        
        // Listen for payment completion via Firestore
        _listenForPaymentCompletion(paymentIntentId);
      }
    }
  }
  
  void _listenForPaymentCompletion(String paymentIntentId) {
    FirebaseFirestore.instance
        .collection('payments')
        .doc(paymentIntentId)
        .snapshots()
        .listen((snapshot) {
      
      if (snapshot.exists) {
        final status = snapshot.data()?['status'];
        
        if (status == 'succeeded') {
          _handlePaymentSuccess();
        } else if (status == 'failed') {
          _handlePaymentFailure();
        }
      }
    });
  }
  
  void _handlePaymentSuccess() {
    // Navigate to success screen
    print('Payment successful!');
  }
  
  void _handlePaymentFailure() {
    // Handle payment failure
    print('Payment failed!');
  }
}

Deep Link Handling in Flutter

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Handle deep links
      onGenerateRoute: (settings) {
        if (settings.name?.startsWith('/payment') == true) {
          return MaterialPageRoute(
            builder: (context) => PaymentResultScreen(
              uri: Uri.parse(settings.name!),
            ),
          );
        }
        return null;
      },
    );
  }
}

class PaymentResultScreen extends StatelessWidget {
  final Uri uri;
  
  const PaymentResultScreen({Key? key, required this.uri}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final paymentIntentId = uri.queryParameters['payment_intent'];
    final status = uri.queryParameters['status'];
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              status == 'succeeded' ? Icons.check_circle : Icons.error,
              size: 64,
              color: status == 'succeeded' ? Colors.green : Colors.red,
            ),
            Text(
              status == 'succeeded' ? 'Payment Successful!' : 'Payment Failed',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            Text('Payment Intent: $paymentIntentId'),
          ],
        ),
      ),
    );
  }
}

Android

<!-- android/app/src/main/AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop"
    android:theme="@style/LaunchTheme">
    
    <!-- Deep link intent filter -->
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="yourapp" />
    </intent-filter>
</activity>