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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ 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.

The private `packages/auth-react-native` package declares the same pins in its own `package.json`; bump both files together, because `src/__tests__/native-sdk-pins.test.ts` fails when they differ and one app can link both packages.

`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.
Expand Down
2 changes: 2 additions & 0 deletions build-native-android.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const modules = [
'sdk-autodetection',
'sdk-nfc',
'sdk-wallet',
'sdk-signals',
'sdk-auth',
];
const args = [
...modules.map(
Expand Down
14 changes: 14 additions & 0 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ target 'SdkReactNativeExample' do
end

post_install do |installer|
# CocoaPods numbers the Pods project's objects with a counter and, once the
# last batch of 100 is used up, restarts it at 0 for the objects added
# afterwards. React Native's `spm_dependency` adds the SwiftPM package
# reference there, so it can take the UUID of an existing object (the
# project root, a target dependency): Xcode then reads Pods.xcodeproj as
# damaged and the build stops with "module map file ... not found" and
# "no such module 'React'". Whether it happens depends only on how many
# objects the pods create, so adding a Swift source file can trigger it.
# Random UUIDs cannot collide with the counter's.
installer.pods_project.instance_variable_set(
:@available_uuids,
Array.new(100) { SecureRandom.hex(12).upcase }
)

react_native_post_install(
installer,
config[:reactNativePath],
Expand Down
51 changes: 48 additions & 3 deletions packages/auth-react-native/DiditAuthReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,63 @@ require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

# Single source of truth for the native iOS version: the same release as the
# verification package's, so both can share one app.
didit_sdk_ios_version = package.dig("diditNativeSdkVersions", "ios").to_s
if didit_sdk_ios_version.empty?
raise "Missing 'diditNativeSdkVersions.ios' in package.json - cannot pin DiditSDK/Auth."
end

# "cocoapods" (default) depends on the DiditSDK/Auth subspec, whose podspec the
# host declares; "spm" adds the DiditAuth product of the sdk-ios package. One
# setting for both Didit packages, as in SdkReactNative.podspec.
didit_sdk_ios_linkage = (
defined?($DiditSdkIosLinkage) && $DiditSdkIosLinkage ?
$DiditSdkIosLinkage.to_s :
ENV.fetch("DIDIT_SDK_IOS_LINKAGE", "cocoapods")
).downcase

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
if didit_sdk_ios_linkage == "spm" && !defined?(spm_dependency)
raise "DiditSdk iOS linkage 'spm' requires React Native 0.75 or newer (spm_dependency is unavailable). Use the 'cocoapods' linkage instead."
end

Pod::Spec.new do |s|
s.name = "DiditAuthReactNative"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = "Didit"
s.platforms = { :ios => min_ios_version_supported }
# DiditAuth needs iOS 15; its hosted pages need 17.4 at run time.
s.platforms = { :ios => [min_ios_version_supported, "15.0"].max_by { |version| Gem::Version.new(version) } }
s.source = { :git => "https://github.com/didit-protocol/sdk-react-native.git", :tag => "auth-react-native-v#{s.version}" }
s.source_files = "ios/**/*.{h,mm}"
s.source_files = "ios/**/*.{h,mm,swift}"
s.private_header_files = "ios/**/*.h"

# No DiditAuth dependency until a reviewed native artifact is available.
s.static_framework = true
s.swift_version = "5.0"

s.pod_target_xcconfig = {
"FRAMEWORK_SEARCH_PATHS" => [
'$(inherited)',
'"${PODS_CONFIGURATION_BUILD_DIR}/DiditSDK"',
'"${PODS_XCFRAMEWORKS_BUILD_DIR}/DiditSDK"',
'"${PODS_XCFRAMEWORKS_BUILD_DIR}/DiditSDK/Auth"'
].join(" ")
}

if didit_sdk_ios_linkage == "spm"
spm_dependency(s,
url: "https://github.com/didit-protocol/sdk-ios.git",
requirement: { kind: "exactVersion", version: didit_sdk_ios_version },
products: ["DiditAuth"]
)
else
s.dependency "DiditSDK/Auth", didit_sdk_ios_version
end

install_modules_dependencies(s)
end
219 changes: 148 additions & 71 deletions packages/auth-react-native/README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,152 @@
# DiditAuth for React Native

**Not ready for authentication or publication.** This private workspace prepares
`@didit-protocol/auth-react-native` as an independent native module. Both native
implementations currently reject every operation. There is no released native
DiditAuth dependency or reviewed initialization/result contract to connect yet.

## Current behavior

The named exports `signIn`, `currentSession`, `getToken`, `signOut` and `stepUp`
take no arguments and return `Promise<never>` in this unavailable-only build.
They reserve the native method names, not the final integration API. Configuration,
options, successful session/token types and cancellation semantics must be added
with the reviewed native contract before publication.

Importing the package without a linked native module is safe. Calling an operation
rejects with `code: 'ERR_DIDIT_AUTH_UNAVAILABLE'`. The native placeholders use the
same code as the JavaScript `DiditAuthUnavailableError`. Match the `code` across
the native boundary; native errors are not instances of the JavaScript class.
Other native rejections propagate unchanged. An unexpected native resolution also
rejects as unavailable, so this build never reports a successful authentication,
signed-out session, token retrieval, logout or step-up.

No credentials are accepted, stored, logged or sent. The package has no dependency
on the verification wrapper or its camera, NFC and location features. It registers
its own `DiditAuthReactNative` TurboModule and `DiditAuthReactNativeSpec` codegen
library. It adds no screens, network calls, URL schemes or OS permissions.

## Local integration

Use React Native's New Architecture. Codegen is checked with React Native 0.77 and
the repository's 0.83 version; platform support remains subject to native builds
and the eventual DiditAuth dependency's minimum versions. Expo Go cannot load the
native module; a development build will be needed for a real native integration.

From the repository root:

```sh
yarn install --immutable
yarn build:auth
yarn workspace @didit-protocol/auth-react-native pack --out /tmp/didit-auth-react-native.tgz
`@didit-protocol/auth-react-native` signs your app's end users up and in with Didit
Authentication. It calls native DiditAuth (`me.didit:didit-auth` on Android,
`DiditSDK/Auth` on iOS): your application's hosted Didit page with PKCE, passkeys,
sessions in the Keychain or Android Keystore, sign-out and step-ups before
sensitive actions. Every request carries the device signals of DiditSignals.

**Not published yet.** The package is private and pins the native SDK release in
`diditNativeSdkVersions` in its `package.json` (4.13.0), which includes DiditAuth.

## Requirements

- React Native 0.77 or later with the New Architecture. Expo Go cannot load the
native module; use a development build.
- iOS 15 or later; the hosted pages (sign-in and step-up checks) need iOS 17.4.
Below 17.4 those calls fail with `browser_unavailable`.
- Android: passkeys need Android 9 or later.

## Set up the redirect

Didit sends the person back to your app with an HTTPS redirect; it accepts no
custom URL schemes.

1. In the Didit console (Authentication, Developers), add the redirect URI, for
example `https://app.example.com/didit/callback`, and copy the publishable key.
2. Android: make that address an App Link of your app and declare DiditAuth's
redirect activity with it in `android/app/src/main/AndroidManifest.xml` (add
`xmlns:tools="http://schemas.android.com/tools"` to the manifest element):

```xml
<activity
android:name="me.didit.sdk.auth.DiditAuthRedirectActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:node="replace">
<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="https" android:host="app.example.com" android:path="/didit/callback" />
</intent-filter>
</activity>
```

Publish `https://app.example.com/.well-known/assetlinks.json` for your package
and signing certificate.
3. iOS: add `webcredentials:app.example.com` to the app's Associated Domains and
serve `https://app.example.com/.well-known/apple-app-site-association` listing
your app under `webcredentials`.
4. Passkeys: add your Android package and certificate fingerprints, and your iOS
app ID, under Native apps in the console.

The native SDK guides have the details: [Android](https://github.com/didit-protocol/sdk-android)
and [iOS](https://github.com/didit-protocol/sdk-ios).

## Install the native SDK

Android resolves `me.didit:didit-auth` from the Didit Maven repository. Add it to the
project-level `settings.gradle` (the verification package needs the same entry):

```groovy
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url "https://raw.githubusercontent.com/didit-protocol/sdk-android/main/repository"
content { includeGroup "me.didit" }
}
}
}
```

On iOS, CocoaPods is the default. DiditSDK is not on the CocoaPods trunk, so the
Podfile names its podspec at the pinned version:

```ruby
pod 'DiditSDK/Auth', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/4.13.0/DiditSDK.podspec'
```

To resolve it with Swift Package Manager instead, set `$DiditSdkIosLinkage = 'spm'`
before `use_native_modules!` and leave the `pod` line out. The package then adds the
`DiditAuth` product of `sdk-ios`.

## Use

```ts
import { createDiditAuth, DiditAuthError } from '@didit-protocol/auth-react-native';

// One per application; reuse it.
const auth = createDiditAuth({
publishableKey: 'pk_live_...',
redirectUri: 'https://app.example.com/didit/callback',
});

const session = await auth.signIn(); // hosted page: sign-up and sign-in
await auth.signInWithPasskey(); // a passkey of this device
const passkeyId = await auth.registerPasskey(); // needs a session at most five minutes old

await auth.currentSession(); // DiditSession or null, cached for 30 seconds
await auth.currentSession({ force: true }); // asks Didit now
const token = await auth.getToken(); // five-minute token for your backend, or null
await auth.signOut();

const result = await auth.stepUp({ action: 'payout', amount: '900.00', currency: 'EUR' });
if (result.approved) {
// Your backend confirms with GET step-up/{result.id}/ before it pays out.
}
```

`stepUp` also takes `{ action: 'bank' }` (adding or changing a bank account) and
`{ action: 'contact' }` (changing the email or phone). The result's `status` is
`not_required`, `approved`, `pending`, `review`, `declined` or `expired`.

A `DiditSession` has `userId`, `sessionId`, `applicationId`, `vendorData`, `email`,
`emailVerified`, `phone`, `methods`, `mfaVerified`, `verified`, `organizations` and
`expiresAt`. Verify the access token on your backend against the issuer's JWKS.
Refresh tokens stay in native storage and never reach JavaScript.

## Errors

Every call rejects with a `DiditAuthError` carrying native DiditAuth's `code` and
`message` unchanged, plus `status`, `fields` and `retryAfter` when Didit sent them:

- the Didit API's own codes, such as `invalid_grant`, `method_unavailable` or
`reauthentication_required`;
- `cancelled`, `invalid_state`, `not_signed_in`, `passkey_cancelled`,
`passkey_unavailable`, `browser_unavailable`, `storage_unavailable`,
`invalid_publishable_key`, `invalid_redirect_uri`, `network_error`,
`rate_limited` (see `retryAfter`) and `server_error`.

```ts
try {
await auth.signIn();
} catch (error) {
if (error instanceof DiditAuthError && error.code === 'cancelled') return;
throw error;
}
```

The archive can be installed in a disposable React Native application for
autolinking checks, either alone or alongside the verification package. `private:
true` blocks registry publication. The root verification release does not include
this workspace. This package uses the existing Bob toolchain for CommonJS, ESM,
TypeScript declarations and React Native source exports.

## Completing the native integration

Before enabling or publishing the package:

1. Connect the reviewed native DiditAuth initialization and method contracts.
Replace the rejecting native implementations and the JavaScript success gate
together. Define typed session results, options and distinct cancellation,
signed-out, expired/revoked and failure outcomes from that contract.
2. Add real distribution coordinates with native versions single-sourced through
`diditNativeSdkVersions`; never borrow the verification package's pins.
3. Keep PKCE/state validation, callback ownership, passkeys, secure token storage,
serialized refresh, logout/revocation and automatic signals in native DiditAuth.
Treat access tokens as opaque values. Never expose refresh tokens to JavaScript.
4. Document and test the native callback/associated-domain setup against the
supported backend contract before adding an Expo configuration plugin. Verify
app/environment isolation, process restoration and callback replay rejection.
5. Run real native sign-in, cancel, session restore/expiry/revocation, concurrent
refresh, sign-out and step-up tests. Capture the launched hosted/native flows
in both themes and required sizes. iOS compilation and device/passkey checks
require a Mac. Codegen and package tests alone do not prove native runtime or
authentication readiness.

The matching Flutter `didit_auth` requires its own package in `sdk-flutter`, with
the same native dependency and acceptance checks. It is not included in this
React Native workspace. Hosted pages, backend contracts and native DiditAuth
remain separate dependencies; this package does not replace their implementations.
Calls also fail with `browser_unavailable` when no screen of the app is in the
foreground. In a build without the native module, such as Expo Go, importing the
package works and every call rejects with `DiditAuthUnavailableError` (code
`ERR_DIDIT_AUTH_UNAVAILABLE`).

## With the verification package

`@didit-protocol/sdk-react-native` and this package pin the same native release, so
both can be installed in one app. On iOS the verification SDK's `DiditSDK` binary
also contains DiditAuth; with both packages the app ships a second copy of
DiditAuth and DiditSignals in the `DiditAuth` binary.
18 changes: 16 additions & 2 deletions packages/auth-react-native/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ if (project.extensions.findByName('kotlin') == null) {
}
apply plugin: "com.facebook.react"

def nativePins = new groovy.json.JsonSlurper().parse(file("../package.json")).diditNativeSdkVersions

android {
namespace "me.didit.authreactnative"
compileSdkVersion authExtOrDefault("compileSdkVersion", 36)
Expand All @@ -39,8 +41,20 @@ android {
}
}

repositories {
// Didit SDK Maven repository, scoped to Didit's group like the verification
// package's: unfiltered, Gradle would probe it for every dependency.
maven {
url "https://raw.githubusercontent.com/didit-protocol/sdk-android/main/repository"
content { includeGroup "me.didit" }
}
}

dependencies {
implementation "com.facebook.react:react-android"
// No DiditAuth artifact is released yet. Keep this unavailable-only module
// independent of verification and add real pins only after native review.

// Didit Authentication; it brings didit-signals only, not the verification SDK.
implementation "me.didit:didit-auth:${nativePins.android}"

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
}
Loading
Loading