Skip to content
Merged
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
39 changes: 37 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
run: yarn prepare

build-android:
name: Released Android compatibility
runs-on: ubuntu-latest

env:
Expand All @@ -70,6 +71,17 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

# Public CI checks the released native ABI. Source integration is checked
# separately with the pinned checkout; released binaries lack the new steps.
- name: Select released native SDK for compatibility check
run: |
node - <<'JS'
const fs = require('fs');
const pkg = require('./package.json');
delete pkg.diditNativeSdkVersions.source;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
JS

- name: Cache turborepo for Android
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
Expand Down Expand Up @@ -117,11 +129,13 @@ jobs:
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"

build-ios:
name: Released iOS compatibility and bridge tests
runs-on: macos-latest

env:
XCODE_VERSION: 26
TURBO_CACHE_DIR: .turbo/ios
DIDIT_SDK_IOS_LOCATION_ENABLED: "false"
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1

Expand All @@ -132,6 +146,17 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

# Public CI checks the released native ABI. Source integration is checked
# separately with the pinned checkout; released binaries lack the new steps.
- name: Select released native SDK for compatibility check
run: |
node - <<'JS'
const fs = require('fs');
const pkg = require('./package.json');
delete pkg.diditNativeSdkVersions.source;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
JS

- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
Expand All @@ -149,13 +174,11 @@ jobs:
fi

- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/ios
bundle install
Expand All @@ -164,3 +187,15 @@ jobs:
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

- name: Test iOS bridge against released native SDK
working-directory: example/ios
run: |
set -o pipefail
SIMULATOR_ID=$(xcrun simctl list devices available | grep -E '^[[:space:]]+iPhone' | grep -oE '[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}' | tail -n 1)
xcodebuild test \
-workspace SdkReactNativeExample.xcworkspace \
-scheme SdkReactNativeExample \
-configuration Debug \
-destination "id=$SIMULATOR_ID" \
-only-testing:SdkReactNativeExampleTests | xcbeautify
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,6 @@ android/generated

# React Native Nitro Modules
nitrogen/

# Native integration artifacts built from the pinned source checkout
.native-sdk/
18 changes: 17 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ Remember to add tests for your change if possible. Run the unit tests by:
yarn test
```

The iOS bridge has XCTests in `example/ios/SdkReactNativeExampleTests`, hosted by the example app. They exercise the native result delivery callback, including `retryBlocked`, and read the built app's location purpose strings. On a Mac, run them after `bundle exec pod install` in `example/ios`:

```sh
cd example/ios
xcodebuild test \
-workspace SdkReactNativeExample.xcworkspace \
-scheme SdkReactNativeExample \
-destination 'platform=iOS Simulator,name=<any iPhone simulator>'
```


### Commit message convention

Expand Down Expand Up @@ -128,7 +138,13 @@ The wrapper version (`version` in `package.json`) and the native Didit SDK versi

This one field drives both iOS resolution paths: the CocoaPods raw-podspec URL and the SwiftPM `exactVersion` requirement (`$DiditSdkIosLinkage = 'spm'`). A bump must therefore have both a matching git tag **and** the SPM release assets (`DiditSDK*.xcframework.zip`) published on that `sdk-ios` release, or SwiftPM consumers fail to resolve.

`SdkReactNative.podspec` and `app.plugin.js` both derive the iOS pin from `diditNativeSdkVersions.ios`, so bumping it there is enough to move the podspec dependency and the sdk-ios podspec URL together. The remaining hand-maintained pins - `android/build.gradle`, the README podspec URL, and the example Podfiles - are asserted against these values by `src/__tests__/native-sdk-pins.test.ts`, so `yarn test` fails if any of them drift (see issues #19 and #28). Only bump `diditNativeSdkVersions.ios` once the matching tag exists in `didit-protocol/sdk-ios`.
`SdkReactNative.podspec` and `app.plugin.js` derive the iOS pin from `diditNativeSdkVersions.ios`; `android/build.gradle` reads `diditNativeSdkVersions.android`. The README's release URL is checked by `src/__tests__/native-sdk-pins.test.ts`. Only bump release pins once the matching native artifacts exist.

During Bank and Location integration, `diditNativeSdkVersions.source` selects a branch at an immutable revision. Set `DIDIT_SDK_SOURCE_PATH` to that clean native checkout. Run `yarn native:android` to publish local Maven artifacts with the native project's own Gradle wrapper. Both examples consume that repository and the source CocoaPod; they reject missing, modified, or mismatched source. No native source is copied into this package. Run both Android examples sequentially, plus the native Bank and Location unit and JVM screenshot suites using that checkout's Gradle wrapper.

For release-binary compatibility testing only, use a disposable wrapper checkout without the `source` field, set `DIDIT_SDK_IOS_LOCATION_ENABLED=false`, and run the same iOS bridge XCTests against the declared release. That checks the `retryBlocked` ABI and callback; it does not verify Bank or Location. Public CI explicitly performs this released-binary check. Integration builds and OS permission captures require the pinned source checkout and a Mac for iOS. Verify English and Spanish when-in-use and temporary precise-location prompts on a device; string-resource tests do not prove the OS dialogs.

Before the stacked push, remove the temporary `source` pin, update the released versions, regenerate both examples, refresh CocoaPods lockfiles on a Mac, and repeat the native callback, permission, build and screenshot checks. Enable Location using CocoaPods until a released SwiftPM Location product is supported here.


### Scripts
Expand Down
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A React Native wrapper for the Didit Identity Verification SDK. Supports both iO

## Permissions

The SDK uses the camera, location, and optionally NFC on both platforms. Native permissions are declared by the underlying native SDKs and merged automatically where the platform supports it.
The SDK uses the camera, optionally NFC, and location only for the Location verification step (see [Bank and Location verification steps](#bank-and-location-verification-steps)). Native permissions are declared by the underlying native SDKs and merged automatically where the platform supports it.

### iOS

Expand Down Expand Up @@ -85,6 +85,90 @@ The SDK handles Android runtime permission requests automatically. When the user

You do not need to request camera permission in your app code before calling `startVerification()` — the SDK manages this internally.

## Bank and Location verification steps

Workflows can include a **Bank** step (the person links a bank account) and a **Location** step (the device position is read once, with the person's consent). Both screens are drawn by the native SDKs, so this package needs no new API: they run inside `startVerification()` / `startVerificationWithWorkflow()`, and the outcome comes back as the same `VerificationResult`.

> **Development preview:** these steps currently use the native integration source pinned in `diditNativeSdkVersions.source` in `package.json`. Set `DIDIT_SDK_SOURCE_PATH` to a clean checkout at that revision before building either example. A missing or different checkout fails the build instead of substituting the 4.9.1 release, which does not contain these steps. The temporary source pin must move to the released native version before the stacked push and package release.

For Android source builds, apply this package's `android/native-source.gradle` from the host's `settings.gradle`. First run `yarn native:android` to build the pinned native revision with its own Gradle wrapper. The settings script exclusively resolves Didit artifacts from that local Maven repository, preventing a fallback to released artifacts. Both examples already do this; Expo prebuild adds it automatically.

### Bank

No setup. The SDK opens the bank's approval page in the system browser (a Custom Tab on Android, Safari in a sheet on iOS) and checks the result itself when the person comes back, so there is no permission, URL scheme or deep link to register.

### Location on iOS

Location lives in the native SDK's optional `DiditSDK/Location` module, which is not included in the `all` variant. Use CocoaPods with `$DiditSdkIosLocationEnabled = true` before `use_native_modules!`, and declare both the selected variant and `DiditSDK/Location` with the same source path. The bare example's Podfile shows this setup. Expo users set `iosLocationEnabled: true` in this package's plugin options and run prebuild. SwiftPM linkage currently cannot opt into Location. Without the module, or without `NSLocationWhenInUseUsageDescription`, the step shows no prompt, reports that the device cannot place the person, and the workflow's own rules decide.

Add the purpose strings to `Info.plist`. `NSLocationTemporaryUsageDescriptionDictionary` is optional: it lets the step ask once for precise location when the person granted only an approximate one (iOS 14+), and its key must be exactly `DiditLocationVerification`.

```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to confirm where you are for this verification.</string>
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>DiditLocationVerification</key>
<string>This verification needs your precise location once.</string>
</dict>
```

iOS picks these strings by the device language, not by the SDK's `languageCode`, so translate them in each `<lang>.lproj/InfoPlist.strings`. The precise-location string is translated under its purpose key. For example `es.lproj/InfoPlist.strings`:

```
"NSLocationWhenInUseUsageDescription" = "Tu ubicación se usa para confirmar dónde estás en esta verificación.";
"DiditLocationVerification" = "Esta verificación necesita tu ubicación precisa una vez.";
```

With Expo, set the same keys in `app.json` and translate them with [`locales`](https://docs.expo.dev/guides/localization/#translating-app-metadata):

```json
{
"expo": {
"plugins": [
["@didit-protocol/sdk-react-native", { "iosLocationEnabled": true }]
],
"ios": {
"infoPlist": {
"NSLocationWhenInUseUsageDescription": "Your location is used to confirm where you are for this verification.",
"NSLocationTemporaryUsageDescriptionDictionary": {
"DiditLocationVerification": "This verification needs your precise location once."
}
}
},
"locales": {
"es": "./locales/es.json"
}
}
}
```

where `locales/es.json` is:

```json
{
"ios": {
"NSLocationWhenInUseUsageDescription": "Tu ubicación se usa para confirmar dónde estás en esta verificación.",
"DiditLocationVerification": "Esta verificación necesita tu ubicación precisa una vez."
}
}
```

Both example apps in this repository declare these strings in English and Spanish.

Once your app links the module, declare **Precise Location** and **Coarse Location** in App Store Connect under **App Functionality**, linked to the user and not used for tracking. The SDK reads one position per attempt, only while the step is on screen, and never in the background.

### Location on Android

The native SDK declares `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION`, plus the location hardware features as optional (`android:required="false"`), and they merge into your app automatically. It asks for them at runtime only when a workflow reaches the step, after a screen that explains why, and the person can grant precise or approximate location. If your app never runs the step, you can remove them from the merged manifest (the `<manifest>` element needs `xmlns:tools="http://schemas.android.com/tools"`):

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

A session that still reaches the step then reports the location as unavailable, without a prompt. If you keep them, declare **Approximate location** and **Precise location** as collected data in your Google Play Data safety form.

## Native SDK Variants

NFC and auto-detection are enabled by default on both platforms.
Expand Down Expand Up @@ -526,6 +610,7 @@ Both `startVerification` and `startVerificationWithWorkflow` return a `Promise<V
| `cameraAccessDenied` | Camera permission not granted |
| `notInitialized` | SDK not initialized (Android only) |
| `apiError` | API request failed |
| `retryBlocked` | A previous verification was not approved and the maximum number of retries was reached |
| `unknown` | Other error with message |

### Handling Results
Expand Down
13 changes: 13 additions & 0 deletions SdkReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ didit_sdk_ios_linkage = (
ENV.fetch("DIDIT_SDK_IOS_LINKAGE", "cocoapods")
).downcase

didit_sdk_ios_location_enabled = (
defined?($DiditSdkIosLocationEnabled) ? $DiditSdkIosLocationEnabled :
ENV.fetch("DIDIT_SDK_IOS_LOCATION_ENABLED", "false").downcase == "true"
)
if didit_sdk_ios_location_enabled && didit_sdk_ios_linkage == "spm"
raise "Location requires CocoaPods linkage with the optional DiditSDK/Location subspec."
end
if package.dig("diditNativeSdkVersions", "source")
Pod::Executable.execute_command('node', [File.join(__dir__, 'native-sdk-source.js'), 'ios'])
raise "The pinned native source requires CocoaPods linkage." if didit_sdk_ios_linkage == "spm"
end

unless ["spm", "cocoapods"].include?(didit_sdk_ios_linkage)
raise "Invalid DiditSdk iOS linkage '#{didit_sdk_ios_linkage}'. Set $DiditSdkIosLinkage or DIDIT_SDK_IOS_LINKAGE to one of: spm, cocoapods."
end
Expand Down Expand Up @@ -107,6 +119,7 @@ Pod::Spec.new do |s|
)
else
s.dependency didit_sdk_subspec, didit_sdk_ios_version
s.dependency "DiditSDK/Location", didit_sdk_ios_version if didit_sdk_ios_location_enabled
end

install_modules_dependencies(s)
Expand Down
7 changes: 6 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ if (project.extensions.findByName('kotlin') == null) {

apply plugin: "com.facebook.react"

def nativePins = new groovy.json.JsonSlurper().parse(file("../package.json")).diditNativeSdkVersions
if (nativePins.source && !gradle.ext.has("diditNativeSourceRepository")) {
throw new GradleException("Apply the wrapper android/native-source.gradle in settings.gradle to use the pinned native source.")
}

def diditSdkAndroidVariantProperty = (
rootProject.findProperty("diditSdkAndroidVariant")
?: findProperty("diditSdkAndroidVariant")
Expand Down Expand Up @@ -132,7 +137,7 @@ dependencies {
implementation "com.facebook.react:react-android"

// Didit native Android SDK
implementation "me.didit:$diditSdkAndroidArtifact:4.9.1"
implementation "me.didit:$diditSdkAndroidArtifact:${nativePins.android}"

// Coroutines (required for state flow observation)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
Expand Down
18 changes: 18 additions & 0 deletions android/native-source.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Apply from the host's settings.gradle before resolving Didit dependencies.
def sourceResolver = new File(buildscript.sourceFile.parentFile.parentFile, 'native-sdk-source.js')
def nativeRepository = providers.exec {
commandLine('node', sourceResolver.absolutePath, 'android-repository')
}.standardOutput.asText.get().trim()

if (nativeRepository) {
gradle.ext.diditNativeSourceRepository = nativeRepository
// Cover both React Native's settings repositories and Expo's project repositories.
def configureRepository = { repositories ->
repositories.exclusiveContent {
forRepository { repositories.maven { url = uri(nativeRepository) } }
filter { includeGroup('me.didit') }
}
}
configureRepository(dependencyResolutionManagement.repositories)
gradle.beforeProject { project -> configureRepository(project.repositories) }
}
Loading
Loading