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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Add the following keys to your app's `Info.plist`:
| Microphone | `NSMicrophoneUsageDescription` | Video recording for liveness checks | Yes |
| Photo Library | `NSPhotoLibraryUsageDescription` | Upload documents from device gallery | Yes |
| NFC | `NFCReaderUsageDescription` | Read NFC chips in passports/ID cards | If using NFC |
| Location | `NSLocationWhenInUseUsageDescription` | Location verification step | If a workflow includes Location |

```xml
<key>NSCameraUsageDescription</key>
Expand All @@ -165,10 +166,19 @@ Add the following keys to your app's `Info.plist`:
<string>Photo library access is required to upload documents.</string>
<key>NFCReaderUsageDescription</key>
<string>NFC is used to read passport chip data for identity verification.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to confirm where you are for this verification.</string>
```

If any required iOS privacy key is missing, iOS terminates the app as soon as the SDK tries to access that protected resource. For example, missing `NSCameraUsageDescription` causes a crash when the user taps the document camera's take photo button.

Without `NSLocationWhenInUseUsageDescription`, iOS never shows the location prompt, so the Location step cannot read the device's location.
iOS shows this text in its own prompt, in the device language, so translate it in your app's `InfoPlist.strings` for every language you support. For example, in `es.lproj/InfoPlist.strings`:

```
"NSLocationWhenInUseUsageDescription" = "Tu ubicación se usa para confirmar dónde estás en esta verificación.";
```

#### NFC Configuration (for passport/ID chip reading)

1. **Add NFC Capability** in Xcode:
Expand Down Expand Up @@ -206,10 +216,21 @@ The following permissions are declared in the native SDK's `AndroidManifest.xml`
| `INTERNET` | Network access for API communication | Yes |
| `ACCESS_NETWORK_STATE` | Detect network availability | Yes |
| `CAMERA` | Document scanning and face verification | Yes |
| `RECORD_AUDIO` | Audio for the video recorded during document and face capture | Yes |
| `VIBRATE` | Haptic feedback while reading an NFC chip | Yes |
| `NFC` | Read NFC chips in passports/ID cards | If using NFC |

Camera and NFC hardware features are declared as optional (`android:required="false"`), so your app can be installed on devices without these features. When `diditSdkAndroidNfcEnabled=false`, the Android NFC permission and feature are not added by the SDK.

If any of your workflows includes a Location step, also declare the location permissions in your app's `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```

Declare both. Since Android 12 the user can choose to share only their approximate location, and Android requires an app that asks for precise location to ask for approximate location too.

#### Runtime Permissions

The SDK handles Android runtime permission requests automatically. When the user reaches a step that requires camera access:
Expand Down Expand Up @@ -423,6 +444,18 @@ final sessionToken = await yourBackend.createSession(userId);
final result = await DiditSdk.startVerification(sessionToken);
```

## Bank and Location Steps

Bank and Location verification are steps of the workflow you configure in the Didit console, so there is nothing to enable in Dart.
When a session's workflow includes them, `startVerification` and `startVerificationWithWorkflow` show them in the same native flow as every other step, in the language set by `languageCode` (for example `'es'`) or the device locale.
They run in the native SDKs, so they need a `didit_sdk` release that pins native SDKs with these steps (see the [changelog](CHANGELOG.md)).

- **Location** asks for the device location on the Location step itself; you do not need to request it in your app first. Add the location entries from [Permissions](#permissions): `NSLocationWhenInUseUsageDescription` on iOS, `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION` on Android. The step reads the location of the device it runs on.
- **Bank** needs no permission entry.
- **Results** keep the same types. A session decided on either step returns `VerificationCompleted` with `approved`, `pending` or `declined`, or `VerificationCancelled` when the user leaves the flow. The result does not say which check declined the session; read the decision from the session through the Didit API, webhooks or console.

If a workflow includes Location, declare location data in your App Store privacy details and in the Google Play Data safety form.

## Verification Results

Both `startVerification` and `startVerificationWithWorkflow` return a `Future<VerificationResult>`. The result is a sealed class — use pattern matching to determine the outcome.
Expand Down
3 changes: 3 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Location verification step: precise location when granted, approximate otherwise. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="sdk_flutter_example"
android:name="${applicationName}"
Expand Down
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
<string>NFC access is required to read the chip in your identity document.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is required to upload document images.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to confirm where you are for this verification.</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>D23300000045737445494420763335</string>
Expand Down
38 changes: 38 additions & 0 deletions test/example_app_permissions_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

// The Location step runs in the native SDKs, but the permission entries it
// needs belong to the host app. The example app must keep them so it can run
// a workflow with a Location step on both platforms.
void main() {
test('example iOS app explains why it asks for the location', () {
final plist = File('example/ios/Runner/Info.plist').readAsStringSync();
final usage = RegExp(
r'<key>NSLocationWhenInUseUsageDescription</key>\s*<string>([^<]*)</string>',
).firstMatch(plist);

expect(usage, isNotNull);
expect(
usage!.group(1),
'Your location is used to confirm where you are for this verification.',
);
});

test('example Android app declares precise and approximate location', () {
final manifest = File(
'example/android/app/src/main/AndroidManifest.xml',
).readAsStringSync();
final permissions = RegExp(
r'<uses-permission\s+android:name="([^"]+)"',
).allMatches(manifest).map((match) => match.group(1)).toSet();

expect(
permissions,
containsAll(<String>[
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION',
]),
);
});
}
33 changes: 33 additions & 0 deletions test/sdk_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ void main() {
expect(platform.lastVerificationConfig?['languageCode'], 'he');
});

testWidgets('both entry points propagate a Spanish device locale', (
tester,
) async {
tester.binding.platformDispatcher.localeTestValue = const Locale(
'es',
'ES',
);
addTearDown(tester.binding.platformDispatcher.clearLocaleTestValue);

await DiditSdk.startVerification('test-token');
await DiditSdk.startVerificationWithWorkflow('test-workflow');

expect(platform.lastVerificationConfig?['languageCode'], 'es');
expect(platform.lastWorkflowConfig?['languageCode'], 'es');
});

test('startVerificationWithWorkflow returns completed result', () async {
final result = await DiditSdk.startVerificationWithWorkflow(
'test-workflow',
Expand Down Expand Up @@ -192,6 +208,23 @@ void main() {
expect(cancelled.session?.sessionId, 'cancelled-session');
});

test('VerificationResult.fromMap maps a declined session and ignores keys '
'it does not know', () {
// A session declined on a server-decided step (Bank, Location) reports
// only the status; a newer native SDK may add keys the wrapper ignores.
final result = VerificationResult.fromMap({
'type': 'completed',
'sessionId': 'declined-session',
'status': 'Declined',
'step': 'location',
});

expect(result, isA<VerificationCompleted>());
final completed = result as VerificationCompleted;
expect(completed.session.sessionId, 'declined-session');
expect(completed.session.status, VerificationStatus.declined);
});

test('submitTransaction returns typed result with actionRequired', () async {
platform.throwValidationError = false;

Expand Down
Loading