From bb701b67bf61043d554d913f51905ffbe024ae4e Mon Sep 17 00:00:00 2001 From: Alejandro Rosas Date: Mon, 12 Oct 2026 08:50:42 +0000 Subject: [PATCH 1/2] DID-6933: [SDKA6] React Native and Flutter: Authentication wrappers Opened by a BotInc run. Delivery follows the workflow's review and merge policy. --- .github/workflows/ci.yml | 6 ++ README.md | 4 + eslint.config.mjs | 2 +- example/package.json | 1 + package.json | 10 +- .../DiditAuthReactNative.podspec | 19 ++++ packages/auth-react-native/LICENSE | 20 ++++ packages/auth-react-native/README.md | 75 +++++++++++++++ .../auth-react-native/android/build.gradle | 46 +++++++++ .../android/src/main/AndroidManifest.xml | 1 + .../didit/authreactnative/DiditAuthModule.kt | 25 +++++ .../didit/authreactnative/DiditAuthPackage.kt | 23 +++++ .../ios/DiditAuthReactNative.h | 4 + .../ios/DiditAuthReactNative.mm | 48 ++++++++++ packages/auth-react-native/package.json | 93 +++++++++++++++++++ .../auth-react-native/react-native.config.js | 11 +++ .../auth-react-native/src/NativeDiditAuth.ts | 16 ++++ .../src/__tests__/availability.test.ts | 42 +++++++++ .../src/__tests__/codegen.test.ts | 60 ++++++++++++ .../src/__tests__/packaging.test.ts | 85 +++++++++++++++++ packages/auth-react-native/src/errors.ts | 9 ++ packages/auth-react-native/src/index.ts | 36 +++++++ .../auth-react-native/tsconfig.build.json | 4 + packages/auth-react-native/tsconfig.json | 7 ++ tsconfig.build.json | 2 +- turbo.json | 18 +++- yarn.lock | 13 +++ 27 files changed, 673 insertions(+), 7 deletions(-) create mode 100644 packages/auth-react-native/DiditAuthReactNative.podspec create mode 100644 packages/auth-react-native/LICENSE create mode 100644 packages/auth-react-native/README.md create mode 100644 packages/auth-react-native/android/build.gradle create mode 100644 packages/auth-react-native/android/src/main/AndroidManifest.xml create mode 100644 packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthModule.kt create mode 100644 packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt create mode 100644 packages/auth-react-native/ios/DiditAuthReactNative.h create mode 100644 packages/auth-react-native/ios/DiditAuthReactNative.mm create mode 100644 packages/auth-react-native/package.json create mode 100644 packages/auth-react-native/react-native.config.js create mode 100644 packages/auth-react-native/src/NativeDiditAuth.ts create mode 100644 packages/auth-react-native/src/__tests__/availability.test.ts create mode 100644 packages/auth-react-native/src/__tests__/codegen.test.ts create mode 100644 packages/auth-react-native/src/__tests__/packaging.test.ts create mode 100644 packages/auth-react-native/src/errors.ts create mode 100644 packages/auth-react-native/src/index.ts create mode 100644 packages/auth-react-native/tsconfig.build.json create mode 100644 packages/auth-react-native/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf642ba..f30431a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,12 @@ jobs: - name: Build package run: yarn prepare + - name: Build authentication package + run: yarn build:auth + + - name: Check authentication package contents + run: yarn workspace @didit-protocol/auth-react-native pack --dry-run + build-android: name: Released Android compatibility runs-on: ubuntu-latest diff --git a/README.md b/README.md index b2ddbdc..d40c378 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A React Native wrapper for the Didit Identity Verification SDK. Supports both iOS and Android with a unified TypeScript API built on React Native's New Architecture (TurboModules). +Authentication is being prepared as the separate +[`@didit-protocol/auth-react-native`](packages/auth-react-native/README.md) package. +It is private and unavailable until the native DiditAuth integration is ready. + ## Requirements - React Native 0.76+ (New Architecture / TurboModules) diff --git a/eslint.config.mjs b/eslint.config.mjs index 16b00bb..9a8df01 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,6 @@ export default defineConfig([ }, }, { - ignores: ['node_modules/', 'lib/'], + ignores: ['node_modules/', 'lib/', 'packages/*/lib/'], }, ]); diff --git a/example/package.json b/example/package.json index ad2f051..9307d0a 100644 --- a/example/package.json +++ b/example/package.json @@ -17,6 +17,7 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", + "@didit-protocol/auth-react-native": "workspace:*", "@react-native-community/cli": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", diff --git a/package.json b/package.json index 8d9c9cb..301927b 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "release": "release-it --only-version", "lint": "eslint \"**/*.{js,ts,tsx}\"", "test": "jest", - "native:android": "node build-native-android.js" + "native:android": "node build-native-android.js", + "build:auth": "yarn workspace @didit-protocol/auth-react-native build" }, "keywords": [ "react-native", @@ -118,7 +119,8 @@ }, "workspaces": [ "example", - "example-expo" + "example-expo", + "packages/auth-react-native" ], "packageManager": "yarn@4.11.0", "react-native-builder-bob": { @@ -183,7 +185,9 @@ "preset": "react-native", "modulePathIgnorePatterns": [ "/example/node_modules", - "/lib/" + "/lib/", + "/packages/auth-react-native/lib/", + "/packages/auth-react-native/node_modules/" ] }, "expo": { diff --git a/packages/auth-react-native/DiditAuthReactNative.podspec b/packages/auth-react-native/DiditAuthReactNative.podspec new file mode 100644 index 0000000..4f61005 --- /dev/null +++ b/packages/auth-react-native/DiditAuthReactNative.podspec @@ -0,0 +1,19 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +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 } + 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.private_header_files = "ios/**/*.h" + + # No DiditAuth dependency until a reviewed native artifact is available. + install_modules_dependencies(s) +end diff --git a/packages/auth-react-native/LICENSE b/packages/auth-react-native/LICENSE new file mode 100644 index 0000000..5b010c8 --- /dev/null +++ b/packages/auth-react-native/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2026 https://didit.me +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/auth-react-native/README.md b/packages/auth-react-native/README.md new file mode 100644 index 0000000..32a1090 --- /dev/null +++ b/packages/auth-react-native/README.md @@ -0,0 +1,75 @@ +# 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` 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 +``` + +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. diff --git a/packages/auth-react-native/android/build.gradle b/packages/auth-react-native/android/build.gradle new file mode 100644 index 0000000..d6b7062 --- /dev/null +++ b/packages/auth-react-native/android/build.gradle @@ -0,0 +1,46 @@ +buildscript { + ext.authExtOrDefault = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${authExtOrDefault('kotlinVersion', '2.0.21')}" + } +} + +apply plugin: "com.android.library" +if (project.extensions.findByName('kotlin') == null) { + apply plugin: "kotlin-android" +} +apply plugin: "com.facebook.react" + +android { + namespace "me.didit.authreactnative" + compileSdkVersion authExtOrDefault("compileSdkVersion", 36) + + defaultConfig { + minSdkVersion authExtOrDefault("minSdkVersion", 24) + targetSdkVersion authExtOrDefault("targetSdkVersion", 36) + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +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. +} diff --git a/packages/auth-react-native/android/src/main/AndroidManifest.xml b/packages/auth-react-native/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94cbbcf --- /dev/null +++ b/packages/auth-react-native/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthModule.kt b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthModule.kt new file mode 100644 index 0000000..c922276 --- /dev/null +++ b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthModule.kt @@ -0,0 +1,25 @@ +package me.didit.authreactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule + +/** Native auth remains unavailable until its reviewed implementation is linked. */ +@ReactModule(name = DiditAuthModule.NAME) +class DiditAuthModule(context: ReactApplicationContext) : NativeDiditAuthSpec(context) { + override fun getName() = NAME + + override fun signIn(promise: Promise) = unavailable(promise) + override fun currentSession(promise: Promise) = unavailable(promise) + override fun getToken(promise: Promise) = unavailable(promise) + override fun signOut(promise: Promise) = unavailable(promise) + override fun stepUp(promise: Promise) = unavailable(promise) + + private fun unavailable(promise: Promise) { + promise.reject("ERR_DIDIT_AUTH_UNAVAILABLE", "DiditAuth is unavailable in this build.") + } + + companion object { + const val NAME = "DiditAuthReactNative" + } +} diff --git a/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt new file mode 100644 index 0000000..7c4c653 --- /dev/null +++ b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt @@ -0,0 +1,23 @@ +package me.didit.authreactnative + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class DiditAuthPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = + if (name == DiditAuthModule.NAME) DiditAuthModule(reactContext) else null + + override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { + mapOf(DiditAuthModule.NAME to ReactModuleInfo( + DiditAuthModule.NAME, + DiditAuthModule::class.java.name, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + true // isTurboModule + )) + } +} diff --git a/packages/auth-react-native/ios/DiditAuthReactNative.h b/packages/auth-react-native/ios/DiditAuthReactNative.h new file mode 100644 index 0000000..e8a26e0 --- /dev/null +++ b/packages/auth-react-native/ios/DiditAuthReactNative.h @@ -0,0 +1,4 @@ +#import + +@interface DiditAuthReactNative : NSObject +@end diff --git a/packages/auth-react-native/ios/DiditAuthReactNative.mm b/packages/auth-react-native/ios/DiditAuthReactNative.mm new file mode 100644 index 0000000..956a5b8 --- /dev/null +++ b/packages/auth-react-native/ios/DiditAuthReactNative.mm @@ -0,0 +1,48 @@ +#import "DiditAuthReactNative.h" + +@implementation DiditAuthReactNative + +RCT_EXPORT_MODULE(DiditAuthReactNative) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +- (void)rejectUnavailable:(RCTPromiseRejectBlock)reject +{ + reject(@"ERR_DIDIT_AUTH_UNAVAILABLE", @"DiditAuth is unavailable in this build.", nil); +} + +- (void)signIn:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self rejectUnavailable:reject]; +} + +- (void)currentSession:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self rejectUnavailable:reject]; +} + +- (void)getToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self rejectUnavailable:reject]; +} + +- (void)signOut:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self rejectUnavailable:reject]; +} + +- (void)stepUp:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self rejectUnavailable:reject]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end diff --git a/packages/auth-react-native/package.json b/packages/auth-react-native/package.json new file mode 100644 index 0000000..dee8231 --- /dev/null +++ b/packages/auth-react-native/package.json @@ -0,0 +1,93 @@ +{ + "name": "@didit-protocol/auth-react-native", + "version": "0.0.0", + "private": true, + "description": "React Native boundary for DiditAuth (native integration pending)", + "main": "./lib/commonjs/index.js", + "module": "./lib/module/index.js", + "types": "./lib/typescript/commonjs/src/index.d.ts", + "react-native": "./src/index.ts", + "exports": { + ".": { + "source": "./src/index.ts", + "react-native": "./src/index.ts", + "import": { + "types": "./lib/typescript/module/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "require": { + "types": "./lib/typescript/commonjs/src/index.d.ts", + "default": "./lib/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android/build.gradle", + "android/src/main/**", + "ios", + "*.podspec", + "react-native.config.js", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], + "scripts": { + "build": "bob build", + "prepack": "bob build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/didit-protocol/sdk-react-native.git", + "directory": "packages/auth-react-native" + }, + "homepage": "https://github.com/didit-protocol/sdk-react-native/tree/main/packages/auth-react-native", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": ">=0.77.0" + }, + "devDependencies": { + "react-native-builder-bob": "^0.40.13", + "typescript": "^5.9.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "commonjs", + { + "esm": true + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "codegenConfig": { + "name": "DiditAuthReactNativeSpec", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "me.didit.authreactnative" + }, + "ios": { + "modulesProvider": { + "DiditAuthReactNative": "DiditAuthReactNative" + } + } + } +} diff --git a/packages/auth-react-native/react-native.config.js b/packages/auth-react-native/react-native.config.js new file mode 100644 index 0000000..7f1e894 --- /dev/null +++ b/packages/auth-react-native/react-native.config.js @@ -0,0 +1,11 @@ +module.exports = { + dependency: { + platforms: { + android: { + packageImportPath: 'import me.didit.authreactnative.DiditAuthPackage;', + packageInstance: 'new DiditAuthPackage()', + }, + ios: {}, + }, + }, +}; diff --git a/packages/auth-react-native/src/NativeDiditAuth.ts b/packages/auth-react-native/src/NativeDiditAuth.ts new file mode 100644 index 0000000..43bf4fd --- /dev/null +++ b/packages/auth-react-native/src/NativeDiditAuth.ts @@ -0,0 +1,16 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +/** + * Unavailable-only seam, not the final native DiditAuth API. + * Arguments and successful results require the reviewed native contract. + */ +export interface Spec extends TurboModule { + signIn(): Promise; + currentSession(): Promise; + getToken(): Promise; + signOut(): Promise; + stepUp(): Promise; +} + +// Importing the package must also work before a native rebuild, or in Expo Go. +export default TurboModuleRegistry.get('DiditAuthReactNative'); diff --git a/packages/auth-react-native/src/__tests__/availability.test.ts b/packages/auth-react-native/src/__tests__/availability.test.ts new file mode 100644 index 0000000..058c258 --- /dev/null +++ b/packages/auth-react-native/src/__tests__/availability.test.ts @@ -0,0 +1,42 @@ +import { TurboModuleRegistry } from 'react-native'; +import { + currentSession, + DiditAuthUnavailableError, + getToken, + signIn, + signOut, + stepUp, +} from '../index'; + +const operations = [signIn, currentSession, getToken, signOut, stepUp]; + +describe('authentication without a linked native module', () => { + it('can be imported before the native application is rebuilt', () => { + expect(TurboModuleRegistry.get('DiditAuthReactNative')).toBeNull(); + expect(operations).toHaveLength(5); + }); + + it.each(operations)( + '%p rejects instead of returning a session or success', + async (operation) => { + await expect(operation()).rejects.toMatchObject({ + name: 'DiditAuthUnavailableError', + code: 'ERR_DIDIT_AUTH_UNAVAILABLE', + message: 'DiditAuth is unavailable in this build.', + }); + } + ); + + it('keeps concurrent and repeated calls unavailable', async () => { + const results = await Promise.allSettled( + [...operations, ...operations].map((operation) => operation()) + ); + + expect(results.every((result) => result.status === 'rejected')).toBe(true); + }); + + it('uses an Error subclass for JavaScript failures', async () => { + await expect(signIn()).rejects.toBeInstanceOf(DiditAuthUnavailableError); + await expect(signOut()).rejects.toBeInstanceOf(Error); + }); +}); diff --git a/packages/auth-react-native/src/__tests__/codegen.test.ts b/packages/auth-react-native/src/__tests__/codegen.test.ts new file mode 100644 index 0000000..7cbd1ac --- /dev/null +++ b/packages/auth-react-native/src/__tests__/codegen.test.ts @@ -0,0 +1,60 @@ +import { mkdtempSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { codegenConfig } from '../../package.json'; + +const specPath = join(__dirname, '..', 'NativeDiditAuth.ts'); +const methods = ['signIn', 'currentSession', 'getToken', 'signOut', 'stepUp']; +const versions = ['react-native-codegen-077', '@react-native/codegen']; + +function generateNativeInterfaces(version: string, outputDirectory: string) { + const { + TypeScriptParser, + } = require(`${version}/lib/parsers/typescript/parser`); + const codegen = require(`${version}/lib/generators/RNCodegen`); + const schema = new TypeScriptParser().parseString( + readFileSync(specPath, 'utf8'), + specPath + ); + + return codegen.generate( + { + libraryName: codegenConfig.name, + schema, + outputDirectory, + packageName: codegenConfig.android.javaPackageName, + }, + { generators: ['modulesAndroid', 'modulesIOS'] } + ); +} + +describe.each(versions)('authentication codegen with %s', (version) => { + it('generates the promise methods consumed by both native implementations', () => { + const directory = mkdtempSync(join(tmpdir(), 'didit-auth-codegen-')); + + try { + expect(generateNativeInterfaces(version, directory)).toBe(true); + verifyNativeSignatures(directory); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); +}); + +function verifyNativeSignatures(directory: string) { + const java = readFileSync( + join(directory, 'java/me/didit/authreactnative/NativeDiditAuthSpec.java'), + 'utf8' + ); + const objc = readFileSync( + join(directory, 'DiditAuthReactNativeSpec/DiditAuthReactNativeSpec.h'), + 'utf8' + ); + + expect(java).toContain('NAME = "DiditAuthReactNative"'); + expect(objc).toContain('NativeDiditAuthSpecJSI'); + methods.forEach((method) => { + expect(java).toContain(`void ${method}(Promise promise)`); + expect(objc).toContain(`(void)${method}:(RCTPromiseResolveBlock)resolve`); + }); +} diff --git a/packages/auth-react-native/src/__tests__/packaging.test.ts b/packages/auth-react-native/src/__tests__/packaging.test.ts new file mode 100644 index 0000000..549e23e --- /dev/null +++ b/packages/auth-react-native/src/__tests__/packaging.test.ts @@ -0,0 +1,85 @@ +import { execFileSync } from 'child_process'; +import { join, resolve } from 'path'; +import manifest from '../../package.json'; + +const root = resolve(__dirname, '../../../..'); +const packageRoot = resolve(__dirname, '../..'); + +function packedFiles(cwd: string): string[] { + const output = execFileSync( + process.execPath, + [ + join(root, '.yarn/releases/yarn-4.11.0.cjs'), + 'pack', + '--dry-run', + '--json', + ], + { cwd, encoding: 'utf8' } + ); + + return output + .trim() + .split('\n') + .map((line) => JSON.parse(line).location) + .filter((location): location is string => typeof location === 'string'); +} + +describe('independent authentication distribution', () => { + it('includes native sources and registration in the archive', () => { + const files = packedFiles(packageRoot); + + expect(files).toEqual( + expect.arrayContaining([ + 'src/index.ts', + 'src/NativeDiditAuth.ts', + 'src/errors.ts', + 'android/src/main/AndroidManifest.xml', + 'android/build.gradle', + 'android/src/main/java/me/didit/authreactnative/DiditAuthModule.kt', + 'android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt', + 'ios/DiditAuthReactNative.h', + 'ios/DiditAuthReactNative.mm', + 'DiditAuthReactNative.podspec', + 'react-native.config.js', + 'LICENSE', + ]) + ); + expect( + files.some((file) => /__tests__|node_modules|\.env/.test(file)) + ).toBe(false); + }); + + it('keeps authentication out of the verification release', () => { + expect(packedFiles(root).some((file) => file.startsWith('packages/'))).toBe( + false + ); + }); + + it('blocks publication and has no verification or credential storage dependency', () => { + expect(manifest.private).toBe(true); + expect(manifest).not.toHaveProperty('dependencies'); + expect(Object.keys(manifest.peerDependencies)).toEqual([ + 'react', + 'react-native', + ]); + }); + + it('autolinks beside verification with distinct native module names', () => { + const script = ` + const config = require('@react-native-community/cli-config').default({}); + const names = ['@didit-protocol/auth-react-native', '@didit-protocol/sdk-react-native']; + console.log(JSON.stringify(names.map(name => config.dependencies[name].platforms))); + `; + const output = execFileSync(process.execPath, ['-e', script], { + cwd: join(root, 'example'), + encoding: 'utf8', + }); + const [auth, verification] = JSON.parse(output); + + expect(auth.android.libraryName).toBe('DiditAuthReactNativeSpec'); + expect(auth.android.packageInstance).toBe('new DiditAuthPackage()'); + expect(auth.ios.podspecPath).toMatch(/DiditAuthReactNative\.podspec$/); + expect(verification.android.libraryName).not.toBe(auth.android.libraryName); + expect(verification.ios.podspecPath).not.toBe(auth.ios.podspecPath); + }); +}); diff --git a/packages/auth-react-native/src/errors.ts b/packages/auth-react-native/src/errors.ts new file mode 100644 index 0000000..e061bd8 --- /dev/null +++ b/packages/auth-react-native/src/errors.ts @@ -0,0 +1,9 @@ +/** Match failures by code: native rejections do not preserve JS subclasses. */ +export class DiditAuthUnavailableError extends Error { + readonly code = 'ERR_DIDIT_AUTH_UNAVAILABLE'; + + constructor() { + super('DiditAuth is unavailable in this build.'); + this.name = 'DiditAuthUnavailableError'; + } +} diff --git a/packages/auth-react-native/src/index.ts b/packages/auth-react-native/src/index.ts new file mode 100644 index 0000000..1df1c4c --- /dev/null +++ b/packages/auth-react-native/src/index.ts @@ -0,0 +1,36 @@ +import NativeDiditAuth, { type Spec } from './NativeDiditAuth'; +import { DiditAuthUnavailableError } from './errors'; + +export { DiditAuthUnavailableError } from './errors'; + +async function unavailable(method: keyof Spec): Promise { + await NativeDiditAuth?.[method]?.(); + + // Never interpret an absent or mismatched native implementation as success. + throw new DiditAuthUnavailableError(); +} + +/** Reserved for native hosted sign-in. Always rejects in this build. */ +export function signIn(): Promise { + return unavailable('signIn'); +} + +/** Unavailability is a failure, not a signed-out session. */ +export function currentSession(): Promise { + return unavailable('currentSession'); +} + +/** Reserved for native token retrieval/refresh. Always rejects in this build. */ +export function getToken(): Promise { + return unavailable('getToken'); +} + +/** Unavailability must not be reported as a successful logout. */ +export function signOut(): Promise { + return unavailable('signOut'); +} + +/** Reserved for native step-up authentication. Always rejects in this build. */ +export function stepUp(): Promise { + return unavailable('stepUp'); +} diff --git a/packages/auth-react-native/tsconfig.build.json b/packages/auth-react-native/tsconfig.build.json new file mode 100644 index 0000000..4dc23fb --- /dev/null +++ b/packages/auth-react-native/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/__tests__"] +} diff --git a/packages/auth-react-native/tsconfig.json b/packages/auth-react-native/tsconfig.json new file mode 100644 index 0000000..ffc12c4 --- /dev/null +++ b/packages/auth-react-native/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src"] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 3c0636a..f8cf000 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig", - "exclude": ["example", "lib"] + "exclude": ["example", "lib", "packages"] } diff --git a/turbo.json b/turbo.json index 39289cf..c8d5434 100644 --- a/turbo.json +++ b/turbo.json @@ -24,7 +24,14 @@ "$TURBO_ROOT$/native-sdk-source.js", "$TURBO_ROOT$/android/build.gradle", "$TURBO_ROOT$/android/native-source.gradle", - "$TURBO_ROOT$/android/src/**" + "$TURBO_ROOT$/android/src/**", + "$TURBO_ROOT$/packages/auth-react-native/package.json", + "$TURBO_ROOT$/packages/auth-react-native/src/**", + "$TURBO_ROOT$/packages/auth-react-native/android/build.gradle", + "$TURBO_ROOT$/packages/auth-react-native/android/src/**", + "$TURBO_ROOT$/packages/auth-react-native/ios/**", + "$TURBO_ROOT$/packages/auth-react-native/*.podspec", + "$TURBO_ROOT$/packages/auth-react-native/react-native.config.js" ], "outputs": [] }, @@ -47,7 +54,14 @@ "$TURBO_ROOT$/native-sdk-source.js", "$TURBO_ROOT$/*.podspec", "$TURBO_ROOT$/ios/**", - "!$TURBO_ROOT$/ios/build/**" + "!$TURBO_ROOT$/ios/build/**", + "$TURBO_ROOT$/packages/auth-react-native/package.json", + "$TURBO_ROOT$/packages/auth-react-native/src/**", + "$TURBO_ROOT$/packages/auth-react-native/android/build.gradle", + "$TURBO_ROOT$/packages/auth-react-native/android/src/**", + "$TURBO_ROOT$/packages/auth-react-native/ios/**", + "$TURBO_ROOT$/packages/auth-react-native/*.podspec", + "$TURBO_ROOT$/packages/auth-react-native/react-native.config.js" ], "outputs": [] } diff --git a/yarn.lock b/yarn.lock index 82985a4..7da8a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,18 @@ __metadata: languageName: node linkType: hard +"@didit-protocol/auth-react-native@workspace:*, @didit-protocol/auth-react-native@workspace:packages/auth-react-native": + version: 0.0.0-use.local + resolution: "@didit-protocol/auth-react-native@workspace:packages/auth-react-native" + dependencies: + react-native-builder-bob: "npm:^0.40.13" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: ">=0.77.0" + languageName: unknown + linkType: soft + "@didit-protocol/sdk-react-native@workspace:*, @didit-protocol/sdk-react-native@workspace:.": version: 0.0.0-use.local resolution: "@didit-protocol/sdk-react-native@workspace:." @@ -12344,6 +12356,7 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@didit-protocol/auth-react-native": "workspace:*" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0" From e80e41b8e15afc54b5518e5f5d0ae6f60b04f88e Mon Sep 17 00:00:00 2001 From: Alejandro Rosas Date: Mon, 12 Oct 2026 09:22:53 +0000 Subject: [PATCH 2/2] DID-6933: [SDKA6] React Native and Flutter: Authentication wrappers Opened by a BotInc run. Delivery follows the workflow's review and merge policy. --- .../didit/authreactnative/DiditAuthPackage.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt index 7c4c653..958493d 100644 --- a/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt +++ b/packages/auth-react-native/android/src/main/java/me/didit/authreactnative/DiditAuthPackage.kt @@ -11,13 +11,30 @@ class DiditAuthPackage : BaseReactPackage() { if (name == DiditAuthModule.NAME) DiditAuthModule(reactContext) else null override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { - mapOf(DiditAuthModule.NAME to ReactModuleInfo( - DiditAuthModule.NAME, - DiditAuthModule::class.java.name, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // isCxxModule - true // isTurboModule - )) + val constructors = ReactModuleInfo::class.java.constructors + val sevenParamCtor = constructors.firstOrNull { it.parameterTypes.size == 7 } + val info = if (sevenParamCtor != null) { + // RN 0.77-0.78 includes hasConstants; newer versions may retain this overload. + sevenParamCtor.newInstance( + DiditAuthModule.NAME, + DiditAuthModule::class.java.name, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // hasConstants + false, // isCxxModule + true // isTurboModule + ) + } else { + // RN 0.79+ also supports registration without hasConstants. + constructors.first { it.parameterTypes.size == 6 }.newInstance( + DiditAuthModule.NAME, + DiditAuthModule::class.java.name, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + true // isTurboModule + ) + } as ReactModuleInfo + mapOf(DiditAuthModule.NAME to info) } }