@smileid/usesmileid_testing 0.0.0-stage → 12.3.1-SNAPSHOT.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ Full release notes: [Smile ID docs](https://docs.usesmileid.com/developer-resources/sdks/mobile/release-notes/react-native-expo).
2
+
3
+ ## Unreleased
4
+
5
+ First release: the native SDKs' test server, for your own tests, and `UseSmileIDTest`, which scripts the selfie on Android and iOS. See the repository CHANGELOG.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Smile ID
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # `@smileid/usesmileid_testing`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Test doubles for the UseSmileID React Native (Expo) SDK: the Android and iOS SDKs' in-process
4
+ test server (`com.usesmileid:testing`, `UseSmileIDTesting`), so your E2E tests run a whole flow
5
+ with no Smile ID backend.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add -D @smileid/usesmileid_testing
11
+ ```
12
+
13
+ Apply its config plugin only in the app config you test with. On iOS it links the
14
+ `UseSmileIDTesting` product in every build configuration (the server throws outside a Debug
15
+ build), so it must not reach the config you ship. For example, in `app.config.js`:
16
+
17
+ ```js
18
+ module.exports = ({ config }) => ({
19
+ ...config,
20
+ plugins: [
21
+ ...(config.plugins ?? []),
22
+ // Your E2E build only.
23
+ ...(process.env.EXPO_PUBLIC_E2E === "1"
24
+ ? ["@smileid/usesmileid_testing"]
25
+ : []),
26
+ ],
27
+ });
28
+ ```
29
+
30
+ The server runs only in a debug build. On Android a release build gets a stub that refuses to
31
+ start, so the test doubles never reach a shipped app, and the plugin lets the debug build's native
32
+ network client reach the server on loopback: it adds `127.0.0.1` and `localhost` to your network
33
+ security config in the debug source set only, or writes a debug-only config when your app names
34
+ none. On iOS, an app built without the plugin gets a module that refuses to start a server.
35
+
36
+ ## Use
37
+
38
+ ```ts
39
+ import {
40
+ UseSmileIDTestServer,
41
+ UseSmileIDTestResponse,
42
+ testToken,
43
+ } from "@smileid/usesmileid_testing";
44
+
45
+ const server = await UseSmileIDTestServer.start();
46
+
47
+ // In the flow's network block:
48
+ // n.config((c) => { c.token = testToken(); ... })
49
+ // n.interceptors((i) => i.testRoute(server))
50
+
51
+ // After the flow:
52
+ expect(server.submissions().map((request) => request.path)).toEqual([
53
+ "/v3/registration",
54
+ ]);
55
+ server.close();
56
+ ```
57
+
58
+ Every request the flow makes goes to the server, whatever host the configuration names, after the
59
+ native network client has signed it. `respond(path, response)`
60
+ scripts `UseSmileIDTestResponse.status(code, body)`, `tokenExpired` (a `401`, so your
61
+ `onTokenExpired` runs) or `connectionDropped`. Once closed, the flow's requests fail rather than
62
+ reach Smile ID.
63
+
64
+ ## A whole flow, with no one in front of the camera
65
+
66
+ `UseSmileIDTest` adds a scripted person to the server: one of the shared scenarios every Smile ID
67
+ SDK runs (`UseSmileIDTestScenario.selfieHappyPath`, `enhancedSelfieHeadTurns`, `selfieTimeout`,
68
+ `skipSubmission` and the document ones) or your own, `UseSmileIDTestScenario.fromJson(text)`.
69
+ `UseSmileIDTestScenario.named("selfie-happy-path")` finds one of them by the name its JSON gives it,
70
+ or returns null.
71
+
72
+ ```ts
73
+ const test = await UseSmileIDTest.create(
74
+ UseSmileIDTestScenario.selfieHappyPath,
75
+ );
76
+
77
+ // In the flow's blocks:
78
+ // n.config((c) => { c.token = test.token; ... })
79
+ // n.interceptors((i) => test.install(i))
80
+ // m.analyzers((a) => {
81
+ // a.forCaptureType(CaptureType.selfie, (face) => face.add(useSmileIDMlkitFace));
82
+ // test.install(a);
83
+ // })
84
+
85
+ test.close();
86
+ ```
87
+
88
+ The scenario answers in place of your selfie analyzer, inside that analyzer's own native session,
89
+ so the capture, the files it writes and the submission are the real ones; any camera frames will
90
+ do, an emulator's included. On iOS that means a phone, since a simulator has no camera. A document
91
+ scenario's `install` throws.
@@ -0,0 +1,51 @@
1
+ // Expo Module — the native SDK's test server (`com.usesmileid:testing`) for React Native tests.
2
+ // It is a debug-only dependency: a release build gets the stub in src/release, which refuses to
3
+ // start a server, so the test doubles and their dependencies never reach a shipped app. An E2E
4
+ // release build (an app's minified test build, never one it ships) opts in with
5
+ // `-PsmileidTestingInRelease=true` and gets the real server instead.
6
+
7
+ plugins {
8
+ id 'com.android.library'
9
+ id 'expo-module-gradle-plugin'
10
+ }
11
+
12
+ def testServerInRelease = (findProperty('smileidTestingInRelease') ?: 'false').toBoolean()
13
+
14
+ group = 'com.usesmileid.testing.rn'
15
+ version = '12.3.0'
16
+
17
+ android {
18
+ namespace 'com.usesmileid.testing.rn'
19
+
20
+ defaultConfig {
21
+ versionCode 1
22
+ versionName '12.3.0'
23
+ consumerProguardFiles 'consumer-rules.pro'
24
+ }
25
+
26
+ sourceSets {
27
+ release {
28
+ java.srcDirs = testServerInRelease ? ['src/debug/java'] : ['src/release/java']
29
+ }
30
+ }
31
+ }
32
+
33
+ dependencies {
34
+ def testServer = [
35
+ 'com.usesmileid:testing:12.3.0-20261026.002955-236',
36
+ // The bridge build the testing artifact runs against, from the same publish: a snapshot names
37
+ // the moving x.y.z-SNAPSHOT bridge, so the build is pinned here as each provider pins it.
38
+ 'com.usesmileid:bridge:12.3.0-20261026.002955-290',
39
+ ]
40
+ testServer.each { debugImplementation it }
41
+ if (testServerInRelease) testServer.each { releaseImplementation it }
42
+ }
43
+
44
+ def smileIdPackageVersion = { File packageDir -> new groovy.json.JsonSlurper().parse(new File(packageDir, 'package.json')).version }
45
+ def bridgeVersion = smileIdPackageVersion(project(':smileid-usesmileid_bridge').projectDir.parentFile)
46
+ def testingVersion = smileIdPackageVersion(projectDir.parentFile)
47
+ if (bridgeVersion != testingVersion) {
48
+ throw new GradleException(
49
+ "@smileid/usesmileid_bridge is $bridgeVersion but ${project.name} is $testingVersion: install every @smileid package at one version."
50
+ )
51
+ }
@@ -0,0 +1,4 @@
1
+ # The Expo module entry point is found by name through autolinking.
2
+ -keep class com.usesmileid.testing.rn.UseSmileIDTestingModule { *; }
3
+ # An E2E release build (-PsmileidTestingInRelease=true) minifies the test server it links.
4
+ -keep class com.usesmileid.testing.** { *; }
@@ -0,0 +1,127 @@
1
+ package com.usesmileid.testing.rn
2
+
3
+ import com.usesmileid.bridge.model.CaptureType
4
+ import com.usesmileid.bridge.registry.FaceAnalyzerFactoryRegistry
5
+ import com.usesmileid.core.models.UseSmileIDResult
6
+ import com.usesmileid.presentation.flow.dsl.UseSmileIDFlowBuilder
7
+ import com.usesmileid.testing.UseSmileIDTest
8
+ import com.usesmileid.testing.UseSmileIDTestPart
9
+ import com.usesmileid.testing.UseSmileIDTestResponse
10
+ import com.usesmileid.testing.UseSmileIDTestScenario
11
+ import com.usesmileid.testing.UseSmileIDTestServer
12
+ import expo.modules.kotlin.modules.Module
13
+ import expo.modules.kotlin.modules.ModuleDefinition
14
+ import java.util.concurrent.ConcurrentHashMap
15
+ import java.util.concurrent.atomic.AtomicInteger
16
+ import kotlinx.coroutines.runBlocking
17
+ import org.json.JSONArray
18
+ import org.json.JSONObject
19
+
20
+ /** The native SDK's test servers by port, and its scripted selfie analyzers by key, for `@smileid/usesmileid_testing`. Debug builds only. */
21
+ class UseSmileIDTestingModule : Module() {
22
+ private val servers = ConcurrentHashMap<Int, UseSmileIDTestServer>()
23
+ private val scriptedFaces = ConcurrentHashMap.newKeySet<String>()
24
+ private val scriptedFaceCount = AtomicInteger()
25
+
26
+ override fun definition() = ModuleDefinition {
27
+ Name("UseSmileIDTesting")
28
+
29
+ AsyncFunction("start") {
30
+ val server = UseSmileIDTestServer()
31
+ servers[server.port] = server
32
+ server.port
33
+ }
34
+
35
+ Function("requests") { port: Int -> requestsJson(server(port)) }
36
+
37
+ Function("respond") { port: Int, path: String, response: String -> server(port).respond(path, parseResponse(response)) }
38
+
39
+ Function("close") { port: Int -> servers.remove(port)?.close() }
40
+
41
+ AsyncFunction("startTest") { name: String, json: String? ->
42
+ val test = UseSmileIDTest(json?.let(UseSmileIDTestScenario::fromJson) ?: bundledScenario(name))
43
+ val selfie = try {
44
+ selfieAnalyzer(test)
45
+ } catch (failure: Exception) {
46
+ test.close()
47
+ throw failure
48
+ }
49
+ val key = "$SCRIPTED_FACE_KEY-${scriptedFaceCount.incrementAndGet()}"
50
+ FaceAnalyzerFactoryRegistry.register(key) { checkNotNull(runBlocking { selfie.newInstance() }) }
51
+ scriptedFaces += key
52
+ servers[test.server.port] = test.server
53
+ mapOf("port" to test.server.port, "faceKey" to key)
54
+ }
55
+
56
+ Function("unscriptFace") { key: String ->
57
+ if (scriptedFaces.remove(key)) FaceAnalyzerFactoryRegistry.unregister(key)
58
+ }
59
+
60
+ // A reloaded runtime leaves no server listening and no scripted analyzer registered.
61
+ OnDestroy {
62
+ servers.values.forEach(UseSmileIDTestServer::close)
63
+ servers.clear()
64
+ scriptedFaces.forEach(FaceAnalyzerFactoryRegistry::unregister)
65
+ scriptedFaces.clear()
66
+ }
67
+ }
68
+
69
+ private fun server(port: Int): UseSmileIDTestServer = servers[port] ?: throw IllegalStateException("No test server on port $port: it was closed or never started")
70
+
71
+ internal companion object {
72
+ const val SCRIPTED_FACE_KEY = "usesmileid-testing-face"
73
+
74
+ private val bundledScenarios = listOf(
75
+ UseSmileIDTestScenario.selfieHappyPath,
76
+ UseSmileIDTestScenario.enhancedSelfieHeadTurns,
77
+ UseSmileIDTestScenario.documentFront,
78
+ UseSmileIDTestScenario.documentFrontAndBack,
79
+ UseSmileIDTestScenario.documentVisa,
80
+ UseSmileIDTestScenario.selfieTimeout,
81
+ UseSmileIDTestScenario.skipSubmission,
82
+ )
83
+
84
+ fun bundledScenario(name: String): UseSmileIDTestScenario =
85
+ bundledScenarios.firstOrNull { it.name == name } ?: throw IllegalArgumentException("No bundled test scenario named $name")
86
+
87
+ /** The selfie analyzer [test] installs, read back through the public `ml { }` block it installs into. */
88
+ fun selfieAnalyzer(test: UseSmileIDTest) =
89
+ when (val registry = UseSmileIDFlowBuilder().ml { analyzers { test.install(into = this) } }) {
90
+ is UseSmileIDResult.Success -> registry.value.getAnalyzersFor(CaptureType.SELFIE).first()
91
+ is UseSmileIDResult.Failure -> throw registry.error
92
+ else -> throw IllegalStateException("The scenario's analyzers didn't install: $registry")
93
+ }
94
+
95
+ fun requestsJson(server: UseSmileIDTestServer): String = JSONArray(
96
+ server.requests.map { request ->
97
+ JSONObject()
98
+ .put("path", request.path)
99
+ .put("headers", JSONObject(request.headers))
100
+ .put(
101
+ "parts",
102
+ JSONObject().apply {
103
+ request.parts.forEach { (name, part) ->
104
+ put(
105
+ name,
106
+ when (part) {
107
+ is UseSmileIDTestPart.Text -> JSONObject().put("text", part.value)
108
+ is UseSmileIDTestPart.File -> JSONObject().put("fileName", part.fileName).put("size", part.size)
109
+ },
110
+ )
111
+ }
112
+ },
113
+ )
114
+ },
115
+ ).toString()
116
+
117
+ fun parseResponse(json: String): UseSmileIDTestResponse {
118
+ val response = JSONObject(json)
119
+ return when (val kind = response.getString("kind")) {
120
+ "status" -> UseSmileIDTestResponse.status(response.getInt("code"), response.optString("body", "{}"))
121
+ "tokenExpired" -> UseSmileIDTestResponse.tokenExpired
122
+ "connectionDropped" -> UseSmileIDTestResponse.connectionDropped
123
+ else -> throw IllegalArgumentException("Unknown test response: $kind")
124
+ }
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,24 @@
1
+ package com.usesmileid.testing.rn
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ /**
7
+ * A release build's stand-in: the test server is a debug-only dependency, so a shipped app can't
8
+ * start one, and the test doubles never reach it.
9
+ */
10
+ class UseSmileIDTestingModule : Module() {
11
+ override fun definition() = ModuleDefinition {
12
+ Name("UseSmileIDTesting")
13
+
14
+ AsyncFunction("start") { -> refuse<Int>() }
15
+ Function("requests") { _: Int -> refuse<String>() }
16
+ Function("respond") { _: Int, _: String, _: String -> refuse<Unit>() }
17
+ Function("close") { _: Int -> }
18
+ AsyncFunction("startTest") { _: String, _: String? -> refuse<Map<String, Any>>() }
19
+ Function("unscriptFace") { _: String -> }
20
+ }
21
+
22
+ /** Typed, because Expo can't register a function whose body only throws. */
23
+ private fun <T> refuse(): T = throw UnsupportedOperationException("UseSmileIDTestServer runs only in a debug build")
24
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,3 @@
1
+ // Entry point Expo discovers when an app references this package in
2
+ // `expo.plugins`. The compiled plugin lives at `plugin/build/index.js`.
3
+ module.exports = require("./plugin/build");
@@ -0,0 +1,3 @@
1
+ export { UseSmileIDTestResponse, UseSmileIDTestServer, testToken, type UseSmileIDTestPart, type UseSmileIDTestRequest, type UseSmileIDTestingNativeModule, } from "./use_smile_id_test_server";
2
+ export { UseSmileIDTest } from "./use_smile_id_test";
3
+ export { UseSmileIDTestScenario } from "./use_smile_id_test_scenario";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";export{UseSmileIDTestResponse,UseSmileIDTestServer,testToken}from"./use_smile_id_test_server";export{UseSmileIDTest}from"./use_smile_id_test";export{UseSmileIDTestScenario}from"./use_smile_id_test_scenario";
@@ -0,0 +1,23 @@
1
+ import { type AnalyzerRegistryBuilder, type InterceptorConfigBuilder } from "@smileid/usesmileid";
2
+ import { UseSmileIDTestServer, type UseSmileIDTestingNativeModule } from "./use_smile_id_test_server";
3
+ import { UseSmileIDTestScenario } from "./use_smile_id_test_scenario";
4
+ /** A flow under test with no Smile ID backend and no person in front of the camera; debug builds only. */
5
+ export declare class UseSmileIDTest {
6
+ /** The person in front of the camera. */
7
+ readonly scenario: UseSmileIDTestScenario;
8
+ /** The backend, in process. */
9
+ readonly server: UseSmileIDTestServer;
10
+ private readonly native;
11
+ private readonly faceKey;
12
+ /** A structurally valid token for the flow's `token`; the test server is the only one that sees it. */
13
+ readonly token: string;
14
+ private constructor();
15
+ /** Starts the native test, its server and its scripted selfie (`selfieHappyPath` for a document scenario). */
16
+ static create(scenario: UseSmileIDTestScenario, native?: UseSmileIDTestingNativeModule): Promise<UseSmileIDTest>;
17
+ /** Routes every request the flow makes to `server`, inside `n.interceptors((i) => …)`. */
18
+ install(into: InterceptorConfigBuilder): void;
19
+ /** Runs the scenario in the selfie analyzer you registered, inside `m.analyzers((a) => …)` after your own; a document scenario throws. */
20
+ install(into: AnalyzerRegistryBuilder): void;
21
+ /** Stops the server, so requests after this are refused, and drops the scripted analyzer. */
22
+ close(): void;
23
+ }
@@ -0,0 +1 @@
1
+ "use strict";import{requireNativeModule as n}from"expo-modules-core";import{AnalyzerFactoryKey as i,faceConfigSchema as o}from"@smileid/usesmileid_bridge";import{CaptureType as c}from"@smileid/usesmileid";import{UseSmileIDTestServer as l,testToken as f}from"./use_smile_id_test_server";const u={[i.MLKitFace]:"UseSmileIDMlkitFace",[i.HuaweiFace]:"UseSmileIDHuaweiFace",[i.VisionFace]:"UseSmileIDVisionFace"};export class UseSmileIDTest{scenario;server;native;faceKey;token=f();constructor(s,e,r,a){this.scenario=s,this.server=e,this.native=r,this.faceKey=a}static async create(s,e=n("UseSmileIDTesting")){const{port:r,faceKey:a}=await e.startTest(s.name,s.script??null);return new UseSmileIDTest(s,l.attach(e,r),e,a)}install(s){if("testRoute"in s){s.testRoute(this.server);return}if(this.scenario.isDocument)throw new Error(`${this.scenario.name}: React Native can't script a document capture yet, only a selfie`);const e=s.build().selfie;if(!e)throw new Error("Register your selfie analyzer before installing the test: the scenario runs in its native session");s.forCaptureType(c.selfie,r=>r.add(m(e,this.faceKey)))}close(){this.server.close(),this.native.unscriptFace(this.faceKey)}}function m(t,s){const e=n(u[t.key]);return{kind:"face",key:t.key,frameProcessorPluginName:t.frameProcessorPluginName,configure:r=>e.configure({...o.parse(r),analyzerFactoryKey:s}),analyzeKameraFrame:r=>e.analyzeKameraFrame(r),reset:()=>e.reset(),flush:()=>e.flush(),close:()=>e.close()}}
@@ -0,0 +1,30 @@
1
+ /** The person in front of the camera, scripted from the shared scenarios every Smile ID SDK runs; any camera frames will do. */
2
+ export declare class UseSmileIDTestScenario {
3
+ /** The scenario's name, as its JSON gives it. */
4
+ readonly name: string;
5
+ private readonly captureType;
6
+ /** @internal A scenario of your own's JSON; undefined for a bundled one, which the native test doubles carry. */
7
+ readonly script?: string | undefined;
8
+ private constructor();
9
+ /** A selfie: hold still, then smile, then the capture completes. */
10
+ static readonly selfieHappyPath: UseSmileIDTestScenario;
11
+ /** An Enhanced SmartSelfie: the head turns, each held, then the capture completes. */
12
+ static readonly enhancedSelfieHeadTurns: UseSmileIDTestScenario;
13
+ /** One side of a document, found and held steady. */
14
+ static readonly documentFront: UseSmileIDTestScenario;
15
+ /** Both sides of a document. */
16
+ static readonly documentFrontAndBack: UseSmileIDTestScenario;
17
+ /** A passport, then the visa page a residency job reads. */
18
+ static readonly documentVisa: UseSmileIDTestScenario;
19
+ /** A selfie that never satisfies the capture, so the flow times out. */
20
+ static readonly selfieTimeout: UseSmileIDTestScenario;
21
+ /** A selfie for a flow with `skipApiSubmission`, which hands the job back instead of submitting it. */
22
+ static readonly skipSubmission: UseSmileIDTestScenario;
23
+ /** The one of the scenarios above whose JSON gives it `name`, such as `selfie-happy-path`, or null. */
24
+ static named(name: string): UseSmileIDTestScenario | null;
25
+ /** A scenario of your own, in the shared schema; throws when `text` isn't one. */
26
+ static fromJson(text: string): UseSmileIDTestScenario;
27
+ /** @internal */
28
+ get isDocument(): boolean;
29
+ toString(): string;
30
+ }
@@ -0,0 +1 @@
1
+ "use strict";import{z as e}from"zod";const a=e.object({name:e.string().min(1),captureType:e.enum(["selfie","document"]),steps:e.array(e.object({emit:e.string().min(1),holdFrames:e.number().int().positive().optional()})).min(1)});export class UseSmileIDTestScenario{name;captureType;script;constructor(n,t,s){this.name=n,this.captureType=t,this.script=s}static selfieHappyPath=new UseSmileIDTestScenario("selfie-happy-path","selfie");static enhancedSelfieHeadTurns=new UseSmileIDTestScenario("ess-head-turn-happy-path","selfie");static documentFront=new UseSmileIDTestScenario("document-happy-path","document");static documentFrontAndBack=new UseSmileIDTestScenario("document-back-side","document");static documentVisa=new UseSmileIDTestScenario("document-visa","document");static selfieTimeout=new UseSmileIDTestScenario("selfie-timeout","selfie");static skipSubmission=new UseSmileIDTestScenario("selfie-skip-submission","selfie");static named(n){return Object.values(UseSmileIDTestScenario).filter(t=>t instanceof UseSmileIDTestScenario).find(t=>t.name===n)??null}static fromJson(n){const t=a.safeParse(JSON.parse(n));if(!t.success)throw new Error(`Not a test scenario: ${t.error.message}`);return new UseSmileIDTestScenario(t.data.name,t.data.captureType,n)}get isDocument(){return this.captureType==="document"}toString(){return`UseSmileIDTestScenario(${this.name})`}}
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import type { UseSmileIDTestRoute } from "@smileid/usesmileid_bridge/internal";
3
+ declare const testPart: z.ZodUnion<readonly [z.ZodObject<{
4
+ text: z.ZodString;
5
+ }, z.core.$strict>, z.ZodObject<{
6
+ fileName: z.ZodString;
7
+ size: z.ZodNumber;
8
+ }, z.core.$strict>]>;
9
+ declare const testRequests: z.ZodArray<z.ZodObject<{
10
+ path: z.ZodString;
11
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
12
+ parts: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
13
+ text: z.ZodString;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ fileName: z.ZodString;
16
+ size: z.ZodNumber;
17
+ }, z.core.$strict>]>>;
18
+ }, z.core.$strip>>;
19
+ /** One part of a request: text, or a file by its name and size. A file's bytes are never kept. */
20
+ export type UseSmileIDTestPart = Readonly<z.infer<typeof testPart>>;
21
+ /** One request the flow made, with a multipart body's parts or a JSON body's top-level fields decoded. */
22
+ export type UseSmileIDTestRequest = Readonly<z.infer<typeof testRequests>[number]>;
23
+ /** What `UseSmileIDTestServer.respond` makes a path answer. */
24
+ export type UseSmileIDTestResponse = {
25
+ readonly kind: "status";
26
+ readonly code: number;
27
+ readonly body: string;
28
+ } | {
29
+ readonly kind: "tokenExpired";
30
+ } | {
31
+ readonly kind: "connectionDropped";
32
+ };
33
+ export declare const UseSmileIDTestResponse: {
34
+ /** An HTTP `code` with `body`, in the API's error shape when it's an error. */
35
+ readonly status: (code: number, body?: string) => UseSmileIDTestResponse;
36
+ /** A `401`, which makes the SDK ask your `onTokenExpired` for a new token. */
37
+ readonly tokenExpired: UseSmileIDTestResponse;
38
+ /** The connection drops before an answer. */
39
+ readonly connectionDropped: UseSmileIDTestResponse;
40
+ };
41
+ /** The native module: Android's `com.usesmileid:testing` and iOS's `UseSmileIDTesting` servers. */
42
+ export type UseSmileIDTestingNativeModule = {
43
+ start(): Promise<number>;
44
+ requests(port: number): string;
45
+ respond(port: number, path: string, response: string): void;
46
+ close(port: number): void;
47
+ /** Starts the native test of the named bundled scenario, or `json`'s: its server's port, and the key its selfie analyzer is under. */
48
+ startTest(name: string, json: string | null): Promise<{
49
+ port: number;
50
+ faceKey: string;
51
+ }>;
52
+ unscriptFace(key: string): void;
53
+ };
54
+ /**
55
+ * The Smile ID backend, in process, for a flow under test. Every `/v3` job endpoint answers the
56
+ * canonical accepted-job payload unless a test scripts something else with `respond`, and every
57
+ * request is recorded, decoded, in `requests()`. It listens on `127.0.0.1` only, in a debug build.
58
+ * Install it with `n.interceptors((i) => i.testRoute(server))` and close it when the test ends.
59
+ */
60
+ export declare class UseSmileIDTestServer implements UseSmileIDTestRoute {
61
+ private readonly native;
62
+ /** The port the server listens on, on `127.0.0.1`. */
63
+ readonly port: number;
64
+ private closed;
65
+ private constructor();
66
+ /** Starts a server. Fails outside a debug build. */
67
+ static start(native?: UseSmileIDTestingNativeModule): Promise<UseSmileIDTestServer>;
68
+ /** @internal The server a native test already started on `port`. */
69
+ static attach(native: UseSmileIDTestingNativeModule, port: number): UseSmileIDTestServer;
70
+ /** The route the SDK reads: this server's port, or undefined once it is closed. */
71
+ testRoutePort(): number | undefined;
72
+ /** Every request the flow made, in order. */
73
+ requests(): readonly UseSmileIDTestRequest[];
74
+ /** The requests to the `/v3` job endpoints: the job submissions. */
75
+ submissions(): readonly UseSmileIDTestRequest[];
76
+ /** Makes `path` answer `response` from now on, instead of what it answers by default. */
77
+ respond(path: string, response: UseSmileIDTestResponse): void;
78
+ /** Stops listening. Requests after this are refused before they leave the process. */
79
+ close(): void;
80
+ }
81
+ /**
82
+ * A structurally valid, far-future token for the flow's `token`; the SDK never checks its signature.
83
+ * A `payload` is the claim binding user details and consent, which the server then injects.
84
+ */
85
+ export declare function testToken(payload?: Readonly<Record<string, unknown>>): string;
86
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";import{requireNativeModule as u}from"expo-modules-core";import{z as e}from"zod";const h=e.union([e.object({text:e.string()}).strict(),e.object({fileName:e.string(),size:e.number().int().nonnegative()}).strict()]),l=e.array(e.object({path:e.string(),headers:e.record(e.string(),e.string()),parts:e.record(e.string(),h)}));export const UseSmileIDTestResponse={status:(r,t="{}")=>({kind:"status",code:r,body:t}),tokenExpired:{kind:"tokenExpired"},connectionDropped:{kind:"connectionDropped"}};const v=["/v3/enhanced_kyc","/v3/registration","/v3/authentication","/v3/biometric_kyc","/v3/document_verification","/v3/enhanced_document_verification","/v3/residency_document_verification"];export class UseSmileIDTestServer{native;port;closed=!1;constructor(t,s){this.native=t,this.port=s}static async start(t=u("UseSmileIDTesting")){return new UseSmileIDTestServer(t,await t.start())}static attach(t,s){return new UseSmileIDTestServer(t,s)}testRoutePort(){return this.closed?void 0:this.port}requests(){const t=l.safeParse(JSON.parse(this.native.requests(this.port)));if(!t.success)throw new Error(`The test server answered requests in a shape it never sends: ${t.error.message}`);return t.data}submissions(){return this.requests().filter(t=>v.some(s=>t.path.startsWith(s)))}respond(t,s){this.native.respond(this.port,t,JSON.stringify(s))}close(){this.closed||(this.closed=!0,this.native.close(this.port))}}export function testToken(r){return[i('{"alg":"HS256","typ":"JWT"}'),i(JSON.stringify({partner_id:"test-partner",exp:4102444800,...r&&{payload:r}})),i("test-signature")].join(".")}const g="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";function i(r){let t="";for(let s=0;s<r.length;s+=3){const[o,a,c]=[0,1,2].map(n=>r.charCodeAt(s+n)),d=o<<16|(a||0)<<8|(c||0),p=s+2<r.length?4:s+1<r.length?3:2;for(let n=0;n<p;n+=1)t+=g[d>>18-6*n&63]}return t}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["android", "apple"],
3
+ "android": {
4
+ "modules": ["com.usesmileid.testing.rn.UseSmileIDTestingModule"]
5
+ },
6
+ "apple": {
7
+ "modules": ["UseSmileIDTestingModule"]
8
+ }
9
+ }
@@ -0,0 +1,58 @@
1
+ require 'json'
2
+
3
+ # Reads the package's package.json so version stays in sync with the
4
+ # JS-side package version. Mirrors the convention used by every Expo
5
+ # Module in this workspace.
6
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
7
+
8
+ Pod::Spec.new do |s|
9
+ # Pod name is intentionally distinct from the SPM product
10
+ # `UseSmileIDTesting` (declared in `expo-module.config.json`)
11
+ # to avoid a name collision between the CocoaPods bridge target
12
+ # and the Swift Package product it imports.
13
+ s.name = 'UseSmileIDTestingExpo'
14
+ s.version = package['version']
15
+ s.summary = 'The native test server for UseSmileID v12 tests (Expo Module bridge).'
16
+ s.description = package['description']
17
+ s.license = package['license']
18
+ s.author = package['author']
19
+ s.homepage = package['homepage']
20
+ s.platforms = { :ios => '15.0' }
21
+ s.swift_version = '5.9'
22
+ s.source = { git: 'https://github.com/usesmileid/react-native-expo.git', tag: "v#{s.version}" }
23
+ s.static_framework = true
24
+
25
+ s.dependency 'ExpoModulesCore'
26
+
27
+ # The Smile ID native deps (`UseSmileIDTesting` / `UseSmileIDBridge`)
28
+ # are SPM-only — declared in `expo-module.config.json` under
29
+ # `apple.dependencies`. Expo Modules autolinking surfaces the SPM
30
+ # products to this target at compile time. Do NOT add a
31
+ # `s.dependency` for them here; that would require shipping a
32
+ # CocoaPods podspec for the binary, which we don't.
33
+
34
+ # Swift flags consistent with Expo Modules' Kotlin/Swift compiler-flag
35
+ # plumbing — toggled by the consuming app's Podfile when it runs in
36
+ # debug mode.
37
+ s.pod_target_xcconfig = {
38
+ # UseSmileIDTesting is an SPM source target whose module no pod searches for; without this, `canImport` is false.
39
+ 'SWIFT_INCLUDE_PATHS' => '$(inherited) "${PODS_CONFIGURATION_BUILD_DIR}"',
40
+ 'DEFINES_MODULE' => 'YES',
41
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule',
42
+ # Hardening — mirrors the native iOS frameworks' Release-config
43
+ # build settings. Dead code is stripped, install-path metadata is
44
+ # removed at link time, debug symbols (DWARF / line tables) are
45
+ # stripped via STRIP_STYLE = 'debugging', and Swift's field-name
46
+ # reflection metadata is dropped so class-dump / Hopper cannot
47
+ # recover Swift property names. STRIP_STYLE must stay 'debugging'
48
+ # (not 'all') — dynamic frameworks need their dyld-visible symbol
49
+ # table intact for consumers to link against. 'all' is for apps.
50
+ 'DEAD_CODE_STRIPPING' => 'YES',
51
+ 'DEPLOYMENT_POSTPROCESSING' => 'YES',
52
+ 'STRIP_INSTALLED_PRODUCT' => 'YES',
53
+ 'STRIP_STYLE' => 'debugging',
54
+ 'SWIFT_REFLECTION_METADATA_LEVEL' => 'without-names'
55
+ }
56
+
57
+ s.source_files = 'UseSmileIDTestingModule/**/*.{h,m,mm,swift,hpp,cpp}'
58
+ end
@@ -0,0 +1,191 @@
1
+ import ExpoModulesCore
2
+ import Foundation
3
+ #if canImport(UseSmileIDTesting)
4
+ import UseSmileID
5
+ import UseSmileIDBridge
6
+ import UseSmileIDTesting
7
+ #endif
8
+
9
+ #if canImport(UseSmileIDTesting)
10
+ /// The native SDK's test servers by port and scripted selfie analyzers by key, for `@smileid/usesmileid_testing`; Debug builds only.
11
+ public final class UseSmileIDTestingModule: Module {
12
+ static let scriptedFaceKey = "usesmileid-testing-face"
13
+
14
+ private let lock = NSLock()
15
+ private var servers: [Int: UseSmileIDTestServer] = [:]
16
+ private var scriptedFaces: Set<String> = []
17
+ private var scriptedFaceCount = 0
18
+
19
+ public func definition() -> ModuleDefinition {
20
+ Name("UseSmileIDTesting")
21
+
22
+ AsyncFunction("start") { () async throws -> Int in
23
+ let server = try UseSmileIDTestServer()
24
+ guard let port = await server.port() else {
25
+ server.close()
26
+ throw TestServerError.didNotStart
27
+ }
28
+ self.lock.withLock { self.servers[Int(port)] = server }
29
+ return Int(port)
30
+ }
31
+
32
+ Function("requests") { (port: Int) throws -> String in
33
+ try Self.requestsJSON(self.server(port).requests)
34
+ }
35
+
36
+ Function("respond") { (port: Int, path: String, response: String) throws in
37
+ try self.server(port).respond(path, with: Self.response(from: response))
38
+ }
39
+
40
+ Function("close") { (port: Int) in
41
+ self.lock.withLock { self.servers.removeValue(forKey: port) }?.close()
42
+ }
43
+
44
+ AsyncFunction("startTest") { (name: String, json: String?) async throws -> [String: Any] in
45
+ let test = try UseSmileIDTest(scenario: json.map(UseSmileIDTestScenario.fromJSON) ?? Self.bundledScenario(name))
46
+ guard let port = await test.server.port() else {
47
+ test.close()
48
+ throw TestServerError.didNotStart
49
+ }
50
+ guard let selfie = test.analyzers.first(where: { $0.captureType == .selfie })?.factories.first else {
51
+ test.close()
52
+ throw TestServerError.noScriptedFace
53
+ }
54
+ let key = self.lock.withLock { () -> String in
55
+ self.scriptedFaceCount += 1
56
+ let key = "\(Self.scriptedFaceKey)-\(self.scriptedFaceCount)"
57
+ self.scriptedFaces.insert(key)
58
+ self.servers[Int(port)] = test.server
59
+ return key
60
+ }
61
+ FaceAnalyzerFactoryRegistry.shared.register(key: key) { ScriptedFace(factory: selfie).analyzeFrame }
62
+ return ["port": Int(port), "faceKey": key]
63
+ }
64
+
65
+ Function("unscriptFace") { (key: String) in
66
+ if self.lock.withLock({ self.scriptedFaces.remove(key) }) != nil {
67
+ FaceAnalyzerFactoryRegistry.shared.unregister(key: key)
68
+ }
69
+ }
70
+
71
+ // A reloaded runtime leaves no server listening and no scripted analyzer registered.
72
+ OnDestroy {
73
+ let (all, keys) = self.lock.withLock { () -> ([UseSmileIDTestServer], Set<String>) in
74
+ defer {
75
+ self.servers.removeAll()
76
+ self.scriptedFaces.removeAll()
77
+ }
78
+ return (Array(self.servers.values), self.scriptedFaces)
79
+ }
80
+ all.forEach { $0.close() }
81
+ keys.forEach { FaceAnalyzerFactoryRegistry.shared.unregister(key: $0) }
82
+ }
83
+ }
84
+
85
+ static func bundledScenario(_ name: String) throws -> UseSmileIDTestScenario {
86
+ let bundled: [UseSmileIDTestScenario] = [
87
+ .selfieHappyPath, .enhancedSelfieHeadTurns, .documentFront, .documentFrontAndBack,
88
+ .documentVisa, .selfieTimeout, .skipSubmission,
89
+ ]
90
+ guard let scenario = bundled.first(where: { $0.name == name }) else { throw TestServerError.noScenario(name) }
91
+ return scenario
92
+ }
93
+
94
+ private func server(_ port: Int) throws -> UseSmileIDTestServer {
95
+ guard let server = lock.withLock({ servers[port] }) else { throw TestServerError.noServer(port) }
96
+ return server
97
+ }
98
+
99
+ static func requestsJSON(_ requests: [UseSmileIDTestRequest]) throws -> String {
100
+ let json: [[String: Any]] = requests.map { request in
101
+ [
102
+ "path": request.path,
103
+ "headers": request.headers,
104
+ "parts": request.parts.mapValues { part -> [String: Any] in
105
+ switch part {
106
+ case let .text(text): ["text": text]
107
+ case let .file(fileName, size): ["fileName": fileName, "size": size]
108
+ }
109
+ },
110
+ ]
111
+ }
112
+ return try String(decoding: JSONSerialization.data(withJSONObject: json), as: UTF8.self)
113
+ }
114
+
115
+ static func response(from json: String) throws -> UseSmileIDTestResponse {
116
+ guard let object = try JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any] else {
117
+ throw TestServerError.unknownResponse(json)
118
+ }
119
+ switch object["kind"] as? String {
120
+ case "status":
121
+ guard let code = object["code"] as? Int else { throw TestServerError.unknownResponse(json) }
122
+ return .status(code: code, body: object["body"] as? String ?? "{}")
123
+ case "tokenExpired": return .tokenExpired
124
+ case "connectionDropped": return .connectionDropped
125
+ default: throw TestServerError.unknownResponse(json)
126
+ }
127
+ }
128
+ }
129
+
130
+ /// One capture session's scripted selfie analyzer, made on its first frame: the registry's provider is synchronous.
131
+ private actor ScriptedFace {
132
+ private let factory: BaseAnalyzerFactory
133
+ private var analyzer: BaseAnalyzer?
134
+
135
+ init(factory: BaseAnalyzerFactory) {
136
+ self.factory = factory
137
+ }
138
+
139
+ nonisolated var analyzeFrame: FaceAnalyzerFactoryRegistry.FaceAnalyzeFrame {
140
+ { input, state in await self.analyze(input, state) }
141
+ }
142
+
143
+ private func analyze(_ input: AnalyzerInput, _ state: IdentityScanState) async -> AnalyzerOutput {
144
+ if analyzer == nil {
145
+ do {
146
+ analyzer = try await factory.newInstance()
147
+ } catch {
148
+ preconditionFailure("The scenario's scripted selfie analyzer didn't start: \(error)")
149
+ }
150
+ guard analyzer != nil else { preconditionFailure("The scenario's scripted selfie analyzer didn't start") }
151
+ }
152
+ return (try? await analyzer?.analyze(data: input, state: state)) ?? FaceDetectorOutput(faces: [])
153
+ }
154
+ }
155
+
156
+ #else
157
+ /// An app built without this package's config plugin has no test server to start: the plugin is
158
+ /// applied only in the app config you test with.
159
+ public final class UseSmileIDTestingModule: Module {
160
+ public func definition() -> ModuleDefinition {
161
+ Name("UseSmileIDTesting")
162
+
163
+ AsyncFunction("start") { () async throws -> Int in throw TestServerError.notLinked }
164
+ Function("requests") { (_: Int) throws -> String in throw TestServerError.notLinked }
165
+ Function("respond") { (_: Int, _: String, _: String) throws in throw TestServerError.notLinked }
166
+ Function("close") { (_: Int) in }
167
+ AsyncFunction("startTest") { (_: String, _: String?) async throws -> [String: Any] in throw TestServerError.notLinked }
168
+ Function("unscriptFace") { (_: String) in }
169
+ }
170
+ }
171
+ #endif
172
+
173
+ enum TestServerError: Error, CustomStringConvertible {
174
+ case didNotStart
175
+ case noServer(Int)
176
+ case unknownResponse(String)
177
+ case notLinked
178
+ case noScriptedFace
179
+ case noScenario(String)
180
+
181
+ var description: String {
182
+ switch self {
183
+ case .notLinked: "This app is built without @smileid/usesmileid_testing's config plugin, so it has no test server"
184
+ case .noScriptedFace: "The scenario's scripted selfie analyzer didn't install"
185
+ case let .noScenario(name): "No bundled test scenario named \(name)"
186
+ case .didNotStart: "The test server didn't start"
187
+ case let .noServer(port): "No test server on port \(port): it was closed or never started"
188
+ case let .unknownResponse(json): "Unknown test response: \(json)"
189
+ }
190
+ }
191
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@smileid/usesmileid_testing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "12.3.1-SNAPSHOT.71",
4
+ "description": "Test doubles for the UseSmileID React Native (Expo) SDK: the native SDKs' in-process test server, for your own E2E tests.",
5
+ "homepage": "https://usesmileid.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/smileidentity/react-native-expo.git",
9
+ "directory": "usesmileid_testing"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/smileidentity/react-native-expo/issues"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Smile Identity <hello@usesmileid.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "expo",
19
+ "expo-module",
20
+ "testing",
21
+ "smile-id"
22
+ ],
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.js"
30
+ },
31
+ "./app.plugin.js": "./app.plugin.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "android/build.gradle",
36
+ "android/consumer-rules.pro",
37
+ "android/src",
38
+ "ios/UseSmileIDTestingExpo.podspec",
39
+ "ios/UseSmileIDTestingModule",
40
+ "plugin/build",
41
+ "app.plugin.js",
42
+ "expo-module.config.json",
43
+ "README.md",
44
+ "LICENSE",
45
+ "CHANGELOG.md"
46
+ ],
47
+ "dependencies": {
48
+ "zod": "^4.6.7",
49
+ "@smileid/usesmileid_bridge": "12.3.1-SNAPSHOT.71"
50
+ },
51
+ "peerDependencies": {
52
+ "expo": ">=56.0.0",
53
+ "expo-modules-core": ">=56.0.0",
54
+ "react-native": ">=0.83.0",
55
+ "@smileid/usesmileid": "12.3.1-SNAPSHOT.71"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^26.6.4",
59
+ "expo": "~57.0.26",
60
+ "expo-modules-core": "^57.0.23",
61
+ "react-native": "^0.86.3",
62
+ "typescript": "^6.0.3",
63
+ "@smileid/usesmileid": "12.3.1-SNAPSHOT.71"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "scripts": {
69
+ "build": "pnpm run build:lib && pnpm run build:plugin && node ../Scripts/copy-assets.mjs",
70
+ "build:lib": "tsc -p tsconfig.json",
71
+ "build:plugin": "tsc -p plugin/tsconfig.json",
72
+ "typecheck": "tsc -b && tsc -p plugin/tsconfig.json --noEmit",
73
+ "test": "vitest run",
74
+ "test:watch": "vitest",
75
+ "clean": "rm -rf dist plugin/build .turbo coverage tsconfig.tsbuildinfo"
76
+ }
6
77
  }
@@ -0,0 +1,31 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ /** The debug-only config this plugin writes when the app names none of its own. */
3
+ export declare const DEBUG_CONFIG_NAME = "smileid_test_network_security";
4
+ /**
5
+ * `config` (a network security config's XML) with cleartext allowed to loopback. A config that
6
+ * already allows both names is returned as it is.
7
+ */
8
+ export declare function allowLoopback(config: string): string;
9
+ /** The XML resource a `@xml/name` reference names, or undefined for anything else. */
10
+ export declare function xmlResourceName(reference: string | undefined): string | undefined;
11
+ /**
12
+ * Writes the debug build's network security config, on every prebuild, so it follows the app's own: the app's own (from `src/main`) plus
13
+ * loopback, under the same name so the debug source set overrides it, or this plugin's own when
14
+ * the app names none, which the debug manifest then points at. Answers the name written.
15
+ */
16
+ export declare function writeDebugConfig(appDirectory: string, ownName: string | undefined, buildType?: string): string;
17
+ /**
18
+ * Points the debug manifest at `name` when the app's own manifest names no network security
19
+ * config, creating the debug manifest if the app has none.
20
+ */
21
+ export declare function writeDebugManifest(appDirectory: string, name: string, buildType?: string): void;
22
+ /**
23
+ * Removes the debug manifest's pointer to this plugin's own config, left by a prebuild from before
24
+ * the app named a config of its own; the debug override of the app's config replaces it.
25
+ */
26
+ export declare function forgetDebugManifest(appDirectory: string, buildType?: string): void;
27
+ /** The Android build types that reach the test server: `debug` unless the app names others. */
28
+ export type AndroidLoopbackOptions = {
29
+ readonly androidBuildTypes?: readonly string[];
30
+ };
31
+ export declare const withAndroidLoopback: ConfigPlugin<AndroidLoopbackOptions | void>;
@@ -0,0 +1,14 @@
1
+ "use strict";var __createBinding=this&&this.__createBinding||(Object.create?(function(n,t,i,e){e===void 0&&(e=i);var r=Object.getOwnPropertyDescriptor(t,i);(!r||("get"in r?!t.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return t[i]}}),Object.defineProperty(n,e,r)}):(function(n,t,i,e){e===void 0&&(e=i),n[e]=t[i]})),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?(function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}):function(n,t){n.default=t}),__importStar=this&&this.__importStar||(function(){var n=function(t){return n=Object.getOwnPropertyNames||function(i){var e=[];for(var r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[e.length]=r);return e},n(t)};return function(t){if(t&&t.__esModule)return t;var i={};if(t!=null)for(var e=n(t),r=0;r<e.length;r++)e[r]!=="default"&&__createBinding(i,t,e[r]);return __setModuleDefault(i,t),i}})();Object.defineProperty(exports,"__esModule",{value:!0}),exports.withAndroidLoopback=exports.DEBUG_CONFIG_NAME=void 0,exports.allowLoopback=allowLoopback,exports.xmlResourceName=xmlResourceName,exports.writeDebugConfig=writeDebugConfig,exports.writeDebugManifest=writeDebugManifest,exports.forgetDebugManifest=forgetDebugManifest;const config_plugins_1=require("expo/config-plugins"),fs=__importStar(require("fs")),path=__importStar(require("path"));exports.DEBUG_CONFIG_NAME="smileid_test_network_security";const LOOPBACK=["127.0.0.1","localhost"],DEBUG_ONLY_CONFIG=`<?xml version="1.0" encoding="utf-8"?>
2
+ <network-security-config>
3
+ <base-config cleartextTrafficPermitted="true" />
4
+ </network-security-config>
5
+ `,LOOPBACK_ONLY_CONFIG=`<?xml version="1.0" encoding="utf-8"?>
6
+ <network-security-config>
7
+ </network-security-config>
8
+ `;function allowLoopback(n){const t=LOOPBACK.filter(e=>!cleartextHosts(n).includes(e));if(t.length===0)return n;const i=[' <domain-config cleartextTrafficPermitted="true">',...t.map(e=>` <domain includeSubdomains="false">${e}</domain>`)," </domain-config>"].join(`
9
+ `);if(!n.includes("</network-security-config>"))throw new Error("[usesmileid_testing] The app's network security config has no closing tag.");return n.replace("</network-security-config>",`${i}
10
+ </network-security-config>`)}function cleartextHosts(n){const t=[];for(const i of n.matchAll(/<domain-config\b([^>]*)>([\s\S]*?)<\/domain-config>/g))if(/cleartextTrafficPermitted="true"/.test(i[1]??""))for(const e of(i[2]??"").matchAll(/<domain\b[^>]*>\s*([^<\s]+)\s*<\/domain>/g))t.push(e[1]);return t}function xmlResourceName(n){return n?.match(/^@xml\/([A-Za-z0-9_]+)$/)?.[1]}function writeDebugConfig(n,t,i="debug"){const e=t??exports.DEBUG_CONFIG_NAME,r=path.join(n,"src","main","res","xml",`${e}.xml`);if(t!==void 0&&!fs.existsSync(r))throw new Error(`[usesmileid_testing] The app names @xml/${e}, but ${r} isn't there to extend. List @smileid/usesmileid_testing after the plugin that writes it, or keep the file in src/main/res/xml.`);const o=t!==void 0?fs.readFileSync(r,"utf-8"):i==="debug"?DEBUG_ONLY_CONFIG:LOOPBACK_ONLY_CONFIG,s=path.join(n,"src",i,"res","xml");return fs.mkdirSync(s,{recursive:!0}),fs.writeFileSync(path.join(s,`${e}.xml`),allowLoopback(o)),e}function writeDebugManifest(n,t,i="debug"){const e=path.join(n,"src",i,"AndroidManifest.xml"),r=`android:networkSecurityConfig="@xml/${t}"`;if(!fs.existsSync(e)){fs.mkdirSync(path.dirname(e),{recursive:!0}),fs.writeFileSync(e,`<manifest xmlns:android="http://schemas.android.com/apk/res/android">
11
+ <application ${r} />
12
+ </manifest>
13
+ `);return}const o=fs.readFileSync(e,"utf-8"),s=o.match(/android:networkSecurityConfig="([^"]*)"/)?.[1];if(s===`@xml/${t}`)return;if(s!==void 0)throw new Error(`[usesmileid_testing] ${e} already names ${s}; add 127.0.0.1 and localhost to it rather than have this plugin replace it.`);const c=/<application\b/.test(o)?o.replace(/<application\b/,`<application ${r}`):o.replace("</manifest>",` <application ${r} />
14
+ </manifest>`);if(c===o)throw new Error(`[usesmileid_testing] ${e} has neither an <application> element nor a closing </manifest> tag to add one before.`);fs.writeFileSync(e,c)}function forgetDebugManifest(n,t="debug"){const i=path.join(n,"src",t,"AndroidManifest.xml");if(!fs.existsSync(i))return;const e=fs.readFileSync(i,"utf-8"),r=e.replace(` android:networkSecurityConfig="@xml/${exports.DEBUG_CONFIG_NAME}"`,"");r!==e&&fs.writeFileSync(i,r)}const withAndroidLoopback=(n,t)=>(0,config_plugins_1.withAndroidManifest)(n,i=>{if(i.modRequest.introspect)return i;const e=config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(i.modResults),r=xmlResourceName(e.$["android:networkSecurityConfig"]),o=path.join(i.modRequest.platformProjectRoot,"app");for(const s of t?.androidBuildTypes??["debug"]){const c=writeDebugConfig(o,r,s);r===void 0?writeDebugManifest(o,c,s):forgetDebugManifest(o,s)}return i});exports.withAndroidLoopback=withAndroidLoopback;
@@ -0,0 +1,7 @@
1
+ import type { ConfigPlugin } from "expo/config-plugins";
2
+ import { type SmileIDSpmOptions } from "@smileid/usesmileid_bridge/plugin";
3
+ import { type AndroidLoopbackOptions } from "./android_loopback";
4
+ export type TestingOptions = SmileIDSpmOptions & AndroidLoopbackOptions;
5
+ export declare const SPM_PRODUCTS: readonly ["UseSmileIDTesting"];
6
+ declare const withTesting: ConfigPlugin<TestingOptions | void>;
7
+ export default withTesting;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.SPM_PRODUCTS=void 0;const plugin_1=require("@smileid/usesmileid_bridge/plugin"),android_loopback_1=require("./android_loopback");exports.SPM_PRODUCTS=["UseSmileIDTesting"];const withIosTesting=(0,plugin_1.withSmileIDSpm)({packageName:"usesmileid_testing",podTarget:"UseSmileIDTestingExpo",products:exports.SPM_PRODUCTS}),withTesting=(i,e)=>(0,android_loopback_1.withAndroidLoopback)(withIosTesting(i,e),e);exports.default=withTesting;