Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:http/http.dart' as http;
import 'package:stripe_example/config.dart';
import 'package:stripe_example/widgets/example_scaffold.dart';
import 'package:stripe_example/widgets/loading_button.dart';

class IdentityVerificationScreen extends StatefulWidget {
const IdentityVerificationScreen({super.key});

@override
State<IdentityVerificationScreen> createState() =>
_IdentityVerificationScreenState();
}

class _IdentityVerificationScreenState
extends State<IdentityVerificationScreen> {
String? _status;

@override
Widget build(BuildContext context) {
return ExampleScaffold(
title: 'Identity Verification',
tags: ['Stripe Identity'],
children: [
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Verify your identity using Stripe Identity. '
'This demonstrates the Identity Verification Sheet which '
'allows users to verify their government-issued ID and selfie.',
style: Theme.of(context).textTheme.bodyMedium,
),
SizedBox(height: 24),
LoadingButton(
onPressed: _startVerification,
text: 'Start Verification',
),
if (_status != null) ...[
SizedBox(height: 24),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getStatusColor().withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _getStatusColor()),
),
child: Text(
_status!,
style: TextStyle(color: _getStatusColor()),
textAlign: TextAlign.center,
),
),
],
],
),
),
],
);
}

Color _getStatusColor() {
if (_status == null) return Colors.grey;
if (_status!.contains('completed')) return Colors.green;
if (_status!.contains('canceled')) return Colors.orange;
return Colors.red;
}

Future<void> _startVerification() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);

try {
// 1. Create VerificationSession on your server
final response = await http.post(
Uri.parse('$kApiUrl/create-verification-session'),
headers: {'Content-Type': 'application/json'},
);

final data = jsonDecode(response.body);

if (data['error'] != null) {
throw Exception(data['error']);
}

final sessionId = data['id'] as String;
final ephemeralKeySecret = data['ephemeral_key_secret'] as String;

// 2. Present the verification sheet
final result = await Stripe.instance.presentIdentityVerificationSheet(
verificationSessionId: sessionId,
ephemeralKeySecret: ephemeralKeySecret,
);

// 3. Handle result
setState(() {
_status = switch (result) {
IdentityVerificationCompleted() =>
'Verification completed successfully!',
IdentityVerificationCanceled() => 'Verification was canceled',
IdentityVerificationFailed(:final error) =>
'Verification failed: ${error.message ?? error.code}',
};
});

if (context.mounted) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(_status!)),
);
}
} catch (e) {
setState(() {
_status = 'Error: $e';
});

if (context.mounted) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
}
}
11 changes: 11 additions & 0 deletions example/lib/screens/screens.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:stripe_example/screens/address_sheet/address_sheet.dart';
import 'package:stripe_example/screens/customer_sheet/customer_sheet_screen.dart';
import 'package:stripe_example/screens/identity_verification/identity_verification_screen.dart';
import 'package:stripe_example/screens/others/can_add_to_wallet_screen.dart';
import 'package:stripe_example/screens/payment_sheet/express_checkout/express_checkout_element.dart';
import 'package:stripe_example/screens/payment_sheet/payment_element/payment_element.dart';
Expand Down Expand Up @@ -347,6 +348,16 @@ class Example extends StatelessWidget {
),
],
),
ExampleSection(
title: 'Identity Verification',
children: [
Example(
title: 'Verify Identity',
builder: (_) => IdentityVerificationScreen(),
platformsSupported: [DevicePlatform.android, DevicePlatform.ios],
),
],
),
ExampleSection(
title: 'Others',
children: [
Expand Down
43 changes: 43 additions & 0 deletions packages/stripe/lib/src/stripe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,49 @@ class Stripe {
return _platform.retrieveCustomerSheetPaymentOptionSelection();
}

/// Present the Identity Verification Sheet
///
/// Before calling this method, create a VerificationSession and
/// ephemeral key on your server.
///
/// Returns [IdentityVerificationResult] indicating the outcome:
/// - [IdentityVerificationCompleted] - User finished verification
/// - [IdentityVerificationCanceled] - User dismissed the sheet
/// - [IdentityVerificationFailed] - An error occurred
///
/// Example:
/// ```dart
/// final result = await Stripe.instance.presentIdentityVerificationSheet(
/// verificationSessionId: 'vs_xxx',
/// ephemeralKeySecret: 'ek_xxx',
/// );
///
/// switch (result) {
/// case IdentityVerificationCompleted():
/// print('Verification completed');
/// case IdentityVerificationCanceled():
/// print('User canceled');
/// case IdentityVerificationFailed(:final error):
/// print('Error: ${error.message}');
/// }
/// ```
///
/// See https://stripe.com/docs/identity for more details.
Future<IdentityVerificationResult> presentIdentityVerificationSheet({
required String verificationSessionId,
required String ephemeralKeySecret,
String? brandLogo,
}) async {
await _awaitForSettings();
return _platform.presentIdentityVerificationSheet(
IdentityVerificationSheetParams(
verificationSessionId: verificationSessionId,
ephemeralKeySecret: ephemeralKeySecret,
brandLogo: brandLogo,
),
);
}

FutureOr<void> _awaitForSettings() {
if (_needsSettings) {
_settingsFuture = applySettings();
Expand Down
1 change: 1 addition & 0 deletions packages/stripe_android/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
implementation "com.stripe:stripe-android:$stripe_version"
implementation "com.stripe:financial-connections:$stripe_version"
implementation "com.stripe:identity:$stripe_version"
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ If you continue to have trouble, follow this discussion to get some support http
promise = Promise(result)
)
}
"presentIdentityVerificationSheet" -> stripeSdk.presentIdentityVerificationSheet(
params = call.requiredArgument("params"),
promise = Promise(result)
)
else -> result.notImplemented()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ private void invoke(String eventName) {
@DoNotStrip
public abstract void openAuthenticatedWebView(String id, String url, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void presentIdentityVerificationSheet(ReadableMap params, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void addListener(String eventType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.facebook.react.module.annotations.ReactModule
import com.flutter.stripe.invoke
import com.reactnativestripesdk.addresssheet.AddressLauncherManager
import com.reactnativestripesdk.customersheet.CustomerSheetManager
import com.reactnativestripesdk.identity.IdentityVerificationSheetManager
import com.reactnativestripesdk.pushprovisioning.PushProvisioningProxy
import com.reactnativestripesdk.utils.ConfirmPaymentErrorType
import com.reactnativestripesdk.utils.CreateTokenErrorType
Expand Down Expand Up @@ -97,6 +98,7 @@ class StripeSdkModule(
private var financialConnectionsSheetManager: FinancialConnectionsSheetManager? = null
private var googlePayLauncherManager: GooglePayLauncherManager? = null
private var googlePayPaymentMethodLauncherManager: GooglePayPaymentMethodLauncherManager? = null
private var identityVerificationSheetManager: IdentityVerificationSheetManager? = null

private var customerSheetManager: CustomerSheetManager? = null

Expand Down Expand Up @@ -1112,6 +1114,27 @@ class StripeSdkModule(
}
}

@ReactMethod
override fun presentIdentityVerificationSheet(
params: ReadableMap,
promise: Promise,
) {
if (!::stripe.isInitialized) {
promise.resolve(createMissingInitError())
return
}

unregisterStripeUIManager(identityVerificationSheetManager)
identityVerificationSheetManager =
IdentityVerificationSheetManager(
reactApplicationContext,
params,
).also {
registerStripeUIManager(it)
it.present(promise)
}
}

@ReactMethod
override fun initCustomerSheet(
params: ReadableMap,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.reactnativestripesdk.identity

import android.annotation.SuppressLint
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.reactnativestripesdk.utils.ErrorType
import com.reactnativestripesdk.utils.StripeUIManager
import com.reactnativestripesdk.utils.createError
import com.reactnativestripesdk.utils.getValOr
import com.stripe.android.core.reactnative.ReactNativeSdkInternal
import com.stripe.android.identity.IdentityVerificationSheet

@OptIn(ReactNativeSdkInternal::class)
class IdentityVerificationSheetManager(
context: ReactApplicationContext,
private val params: ReadableMap,
) : StripeUIManager(context) {

override fun onPresent() {
val activity = getCurrentActivityOrResolveWithError(promise) ?: return

val verificationSessionId = getValOr(params, "verificationSessionId", null)
val ephemeralKeySecret = getValOr(params, "ephemeralKeySecret", null)

if (verificationSessionId.isNullOrEmpty()) {
promise?.resolve(createError(ErrorType.Failed.toString(), "verificationSessionId is required"))
return
}

if (ephemeralKeySecret.isNullOrEmpty()) {
promise?.resolve(createError(ErrorType.Failed.toString(), "ephemeralKeySecret is required"))
return
}

// Build configuration
val configBuilder = IdentityVerificationSheet.Configuration.Builder()

// Brand logo handling would require converting base64 to drawable
// which is complex on Android - skipping for now

@SuppressLint("RestrictedApi")
val identitySheet = IdentityVerificationSheet.create(
activity,
signal,
configBuilder.build(),
::handleResult
)

identitySheet.present(
verificationSessionId = verificationSessionId,
ephemeralKeySecret = ephemeralKeySecret
)
}

private fun handleResult(result: IdentityVerificationSheet.VerificationFlowResult) {
val resultMap = when (result) {
is IdentityVerificationSheet.VerificationFlowResult.Completed -> {
Arguments.createMap().apply {
putString("status", "completed")
}
}
is IdentityVerificationSheet.VerificationFlowResult.Canceled -> {
Arguments.createMap().apply {
putString("status", "canceled")
}
}
is IdentityVerificationSheet.VerificationFlowResult.Failed -> {
Arguments.createMap().apply {
putString("status", "failed")
putMap("error", Arguments.createMap().apply {
putString("code", "failed")
putString("message", result.throwable.message)
putString("localizedMessage", result.throwable.localizedMessage)
})
}
}
}
promise?.resolve(resultMap)
}

companion object {
internal fun createMissingInitError() = createError(
ErrorType.Failed.toString(),
"Identity verification not initialized"
)
}
}
1 change: 1 addition & 0 deletions packages/stripe_ios/ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ GeneratedPluginRegistrant.m
*.mode1v3
*.mode2v3
*.perspectivev3
.build

!default.pbxuser
!default.mode1v3
Expand Down
4 changes: 2 additions & 2 deletions packages/stripe_ios/ios/stripe_ios/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/stripe_ios/ios/stripe_ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let package = Package(
.product(name: "StripePaymentSheet", package: "stripe-ios-spm"),
.product(name: "StripeApplePay", package: "stripe-ios-spm"),
.product(name: "StripeFinancialConnections", package: "stripe-ios-spm"),
.product(name: "StripeIdentity", package: "stripe-ios-spm"),
"stripe_objc"
],
resources: []
Expand Down
Loading
Loading