@spare-technologies/spare-link 1.0.1 → 1.1.0
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/README.md +34 -5
- package/lib/commonjs/core/services/hosted-flow-controller.js +5 -0
- package/lib/commonjs/core/services/hosted-flow-controller.js.map +1 -1
- package/lib/commonjs/internal/hosted/bridge-message.js +13 -0
- package/lib/commonjs/internal/hosted/bridge-message.js.map +1 -1
- package/lib/commonjs/internal/telemetry/native-telemetry-reply.js +59 -0
- package/lib/commonjs/internal/telemetry/native-telemetry-reply.js.map +1 -0
- package/lib/commonjs/internal/telemetry/risk-telemetry-collector.js +99 -0
- package/lib/commonjs/internal/telemetry/risk-telemetry-collector.js.map +1 -0
- package/lib/commonjs/internal/telemetry/risk-telemetry-probes.js +134 -0
- package/lib/commonjs/internal/telemetry/risk-telemetry-probes.js.map +1 -0
- package/lib/commonjs/ui/hosted/hosted-payment-page.js +8 -0
- package/lib/commonjs/ui/hosted/hosted-payment-page.js.map +1 -1
- package/lib/commonjs/ui/hosted/hosted-payment-webview.js +31 -2
- package/lib/commonjs/ui/hosted/hosted-payment-webview.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/core/services/hosted-flow-controller.js +5 -0
- package/lib/module/core/services/hosted-flow-controller.js.map +1 -1
- package/lib/module/internal/hosted/bridge-message.js +13 -0
- package/lib/module/internal/hosted/bridge-message.js.map +1 -1
- package/lib/module/internal/telemetry/native-telemetry-reply.js +53 -0
- package/lib/module/internal/telemetry/native-telemetry-reply.js.map +1 -0
- package/lib/module/internal/telemetry/risk-telemetry-collector.js +94 -0
- package/lib/module/internal/telemetry/risk-telemetry-collector.js.map +1 -0
- package/lib/module/internal/telemetry/risk-telemetry-probes.js +129 -0
- package/lib/module/internal/telemetry/risk-telemetry-probes.js.map +1 -0
- package/lib/module/ui/hosted/hosted-payment-page.js +8 -0
- package/lib/module/ui/hosted/hosted-payment-page.js.map +1 -1
- package/lib/module/ui/hosted/hosted-payment-webview.js +32 -3
- package/lib/module/ui/hosted/hosted-payment-webview.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/core/services/hosted-flow-controller.d.ts.map +1 -1
- package/lib/typescript/commonjs/internal/hosted/bridge-message.d.ts +12 -1
- package/lib/typescript/commonjs/internal/hosted/bridge-message.d.ts.map +1 -1
- package/lib/typescript/commonjs/internal/telemetry/native-telemetry-reply.d.ts +33 -0
- package/lib/typescript/commonjs/internal/telemetry/native-telemetry-reply.d.ts.map +1 -0
- package/lib/typescript/commonjs/internal/telemetry/risk-telemetry-collector.d.ts +47 -0
- package/lib/typescript/commonjs/internal/telemetry/risk-telemetry-collector.d.ts.map +1 -0
- package/lib/typescript/commonjs/internal/telemetry/risk-telemetry-probes.d.ts +19 -0
- package/lib/typescript/commonjs/internal/telemetry/risk-telemetry-probes.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hosted/hosted-payment-page.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hosted/hosted-payment-webview.d.ts +9 -0
- package/lib/typescript/commonjs/ui/hosted/hosted-payment-webview.d.ts.map +1 -1
- package/lib/typescript/commonjs/version.d.ts +1 -1
- package/lib/typescript/module/core/services/hosted-flow-controller.d.ts.map +1 -1
- package/lib/typescript/module/internal/hosted/bridge-message.d.ts +12 -1
- package/lib/typescript/module/internal/hosted/bridge-message.d.ts.map +1 -1
- package/lib/typescript/module/internal/telemetry/native-telemetry-reply.d.ts +33 -0
- package/lib/typescript/module/internal/telemetry/native-telemetry-reply.d.ts.map +1 -0
- package/lib/typescript/module/internal/telemetry/risk-telemetry-collector.d.ts +47 -0
- package/lib/typescript/module/internal/telemetry/risk-telemetry-collector.d.ts.map +1 -0
- package/lib/typescript/module/internal/telemetry/risk-telemetry-probes.d.ts +19 -0
- package/lib/typescript/module/internal/telemetry/risk-telemetry-probes.d.ts.map +1 -0
- package/lib/typescript/module/ui/hosted/hosted-payment-page.d.ts.map +1 -1
- package/lib/typescript/module/ui/hosted/hosted-payment-webview.d.ts +9 -0
- package/lib/typescript/module/ui/hosted/hosted-payment-webview.d.ts.map +1 -1
- package/lib/typescript/module/version.d.ts +1 -1
- package/package.json +24 -4
- package/plugins/with-spare-link.js +78 -5
- package/src/core/services/hosted-flow-controller.ts +5 -0
- package/src/internal/hosted/bridge-message.ts +20 -1
- package/src/internal/telemetry/native-telemetry-reply.ts +59 -0
- package/src/internal/telemetry/risk-telemetry-collector.ts +128 -0
- package/src/internal/telemetry/risk-telemetry-probes.ts +159 -0
- package/src/ui/hosted/hosted-payment-page.tsx +8 -0
- package/src/ui/hosted/hosted-payment-webview.tsx +42 -4
- package/src/version.ts +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The page posts to `window.ReactNativeWebView.postMessage` as a JSON string,
|
|
9
9
|
* which arrives here as `event.nativeEvent.data`.
|
|
10
10
|
*/
|
|
11
|
-
export type HostedBridgeMessage = HostedReady | HostedOpenUrl | HostedCancel | HostedError;
|
|
11
|
+
export type HostedBridgeMessage = HostedReady | HostedOpenUrl | HostedCancel | HostedError | HostedTelemetryRequest;
|
|
12
12
|
/** The payable page is interactive. */
|
|
13
13
|
export interface HostedReady {
|
|
14
14
|
readonly type: 'ready';
|
|
@@ -33,6 +33,17 @@ export interface HostedError {
|
|
|
33
33
|
readonly type: 'error';
|
|
34
34
|
readonly code: string;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The page is asking for this device's native risk facts.
|
|
38
|
+
*
|
|
39
|
+
* The one message that expects an answer — not over this channel, which cannot
|
|
40
|
+
* reply, but through the `window.__spareLinkNativeTelemetry` callback the page
|
|
41
|
+
* exposes and the shell invokes by injecting script. A shell that stays silent
|
|
42
|
+
* costs the page one bounded timeout and nothing else.
|
|
43
|
+
*/
|
|
44
|
+
export interface HostedTelemetryRequest {
|
|
45
|
+
readonly type: 'telemetryRequest';
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Parses a message posted by the hosted page.
|
|
38
49
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-message.d.ts","sourceRoot":"","sources":["../../../../../src/internal/hosted/bridge-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"bridge-message.d.ts","sourceRoot":"","sources":["../../../../../src/internal/hosted/bridge-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,aAAa,GACb,YAAY,GACZ,WAAW,GACX,sBAAsB,CAAC;AAE3B,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA2BjF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivers a telemetry reply into the hosted page.
|
|
3
|
+
*
|
|
4
|
+
* The WebView message channel is one-way, so the reply travels the other way:
|
|
5
|
+
* the page exposes `window.__spareLinkNativeTelemetry` before it asks, and the
|
|
6
|
+
* shell answers by injecting a call to it. A page without the callback — an
|
|
7
|
+
* older page, or one that already gave up waiting — receives nothing and the
|
|
8
|
+
* injected chunk evaluates to a harmless literal.
|
|
9
|
+
*/
|
|
10
|
+
/** Page-exposed global the shell invokes with its telemetry reply. */
|
|
11
|
+
export declare const NATIVE_TELEMETRY_CALLBACK_NAME = "__spareLinkNativeTelemetry";
|
|
12
|
+
/**
|
|
13
|
+
* Builds the script that hands [payload] to the page's telemetry callback.
|
|
14
|
+
*
|
|
15
|
+
* The payload is embedded as a JSON literal. U+2028 and U+2029 are legal inside
|
|
16
|
+
* a JSON string but terminate a statement when evaluated as script, so they are
|
|
17
|
+
* escaped; the result is a single expression that is inert on a page without
|
|
18
|
+
* the callback.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildNativeTelemetryReplyScript(payload: Record<string, unknown>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Builds the handler the webview runs when the page asks for telemetry.
|
|
23
|
+
*
|
|
24
|
+
* Collection and injection are injected so the handler is a pure seam: the
|
|
25
|
+
* component wires the real collector and the WebView reference into it. A
|
|
26
|
+
* collector that answers nothing, or throws, injects nothing — the page's own
|
|
27
|
+
* timeout settles the request.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createTelemetryRequestHandler(deps: {
|
|
30
|
+
collect: () => Promise<Record<string, unknown> | null>;
|
|
31
|
+
inject: (script: string) => void;
|
|
32
|
+
}): () => Promise<void>;
|
|
33
|
+
//# sourceMappingURL=native-telemetry-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-telemetry-reply.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/native-telemetry-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQxF;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACvD,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAgBtB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects this device's native risk facts for the hosted payment page.
|
|
3
|
+
*
|
|
4
|
+
* Every probe is injected, so the collector runs under Jest with fakes and the
|
|
5
|
+
* module wiring lives in `risk-telemetry-probes`. Every probe is also allowed to
|
|
6
|
+
* fail or hang: a throwing or slow reading leaves its fact absent, because a risk
|
|
7
|
+
* reading must never delay or break a payment.
|
|
8
|
+
*
|
|
9
|
+
* The payload mirrors the page's own risk contract: camelCase keys under a
|
|
10
|
+
* `deviceInformation` block, facts only, nothing fabricated.
|
|
11
|
+
*/
|
|
12
|
+
/** How long any single probe may take before its fact is given up on. */
|
|
13
|
+
export declare const TELEMETRY_PROBE_BUDGET_MS = 800;
|
|
14
|
+
/** Identity and build facts read from the platform. */
|
|
15
|
+
export interface RiskDeviceSnapshot {
|
|
16
|
+
readonly operatingSystem: string | null;
|
|
17
|
+
readonly operatingSystemVersion: string | null;
|
|
18
|
+
readonly model: string | null;
|
|
19
|
+
readonly manufacturer: string | null;
|
|
20
|
+
/** A per-vendor install-stable identifier, when the platform offers one. */
|
|
21
|
+
readonly vendorIdentifier: string | null;
|
|
22
|
+
/** False on simulators and emulators. */
|
|
23
|
+
readonly isPhysicalDevice: boolean | null;
|
|
24
|
+
}
|
|
25
|
+
/** A battery reading, with the level as a 0 to 1 fraction. */
|
|
26
|
+
export interface RiskBatteryReading {
|
|
27
|
+
readonly levelFraction: number | null;
|
|
28
|
+
readonly isCharging: boolean | null;
|
|
29
|
+
}
|
|
30
|
+
/** The network medium classes the risk payload distinguishes. */
|
|
31
|
+
export type RiskConnectionKind = 'wifi' | 'cellular' | 'other';
|
|
32
|
+
/** The probes the collector runs. Each may answer null, throw, or hang. */
|
|
33
|
+
export interface RiskTelemetryProbes {
|
|
34
|
+
loadDeviceSnapshot(): Promise<RiskDeviceSnapshot | null>;
|
|
35
|
+
loadBattery(): Promise<RiskBatteryReading | null>;
|
|
36
|
+
loadConnection(): Promise<RiskConnectionKind | null>;
|
|
37
|
+
localeTag(): string | null;
|
|
38
|
+
localDateTime(): string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collects whatever the device answered within the per-probe budget.
|
|
42
|
+
*
|
|
43
|
+
* Returns null when nothing answered, so a caller hands the page nothing rather
|
|
44
|
+
* than an empty block.
|
|
45
|
+
*/
|
|
46
|
+
export declare function collectRiskTelemetry(probes: RiskTelemetryProbes): Promise<Record<string, unknown> | null>;
|
|
47
|
+
//# sourceMappingURL=risk-telemetry-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk-telemetry-collector.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/risk-telemetry-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,uDAAuD;AACvD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,yCAAyC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3C;AAED,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAED,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;AAE/D,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACzD,WAAW,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAClD,cAAc,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,IAAI,MAAM,GAAG,IAAI,CAAC;CAChC;AA4BD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAgDzC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real device probes behind `collectRiskTelemetry`.
|
|
3
|
+
*
|
|
4
|
+
* Every expo module here is an optional peer, loaded lazily inside a try/catch:
|
|
5
|
+
* a host app that ships without one simply reports that fact as absent, and no
|
|
6
|
+
* import of this file can throw for want of a native module. Structural `*Like`
|
|
7
|
+
* types keep this file honest about how little of each module it relies on.
|
|
8
|
+
*/
|
|
9
|
+
import type { RiskTelemetryProbes } from './risk-telemetry-collector';
|
|
10
|
+
/**
|
|
11
|
+
* Formats a date as local-time ISO 8601 with a numeric UTC offset, e.g.
|
|
12
|
+
* `2026-08-25T10:45:46.319+05:30` — never the UTC `Z` form. This keeps the
|
|
13
|
+
* timestamp on the payer's own clock so it reads the same as the device shows,
|
|
14
|
+
* and matches the Flutter SDK's `deviceLocalDateTime`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatLocalIsoWithOffset(date: Date): string;
|
|
17
|
+
/** The production probe set, ready for `collectRiskTelemetry`. */
|
|
18
|
+
export declare function createRiskTelemetryProbes(): RiskTelemetryProbes;
|
|
19
|
+
//# sourceMappingURL=risk-telemetry-probes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk-telemetry-probes.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/risk-telemetry-probes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAIV,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AA8GpC;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAc3D;AAUD,kEAAkE;AAClE,wBAAgB,yBAAyB,IAAI,mBAAmB,CAE/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-payment-page.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4CAA4C,CAAC;
|
|
1
|
+
{"version":3,"file":"hosted-payment-page.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4CAA4C,CAAC;AAevF,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,oBAAoB,CAAC;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,CA6I5E"}
|
|
@@ -22,6 +22,15 @@ export interface HostedPaymentWebViewProps {
|
|
|
22
22
|
* this component stays a transport.
|
|
23
23
|
*/
|
|
24
24
|
onBridgeMessage: (message: unknown) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Answers the page's telemetry request with this device's native risk facts.
|
|
27
|
+
*
|
|
28
|
+
* The reply travels back by injected script rather than a return value, so it
|
|
29
|
+
* is handled here where the webview reference lives, and the message never
|
|
30
|
+
* reaches [onBridgeMessage]. Absent, or answering null, means the page
|
|
31
|
+
* proceeds with what the browser alone can see.
|
|
32
|
+
*/
|
|
33
|
+
onTelemetryRequest?: () => Promise<Record<string, unknown> | null>;
|
|
25
34
|
/** Called when the page itself could not be loaded. */
|
|
26
35
|
onLoadFailure?: () => void;
|
|
27
36
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-payment-webview.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-webview.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hosted-payment-webview.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-webview.tsx"],"names":[],"mappings":"AAwBA;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE3F;AAED,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,GAAG,CAAC,OAAO,CAuHlF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-flow-controller.d.ts","sourceRoot":"","sources":["../../../../../src/core/services/hosted-flow-controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,eAAe,EAAqC,MAAM,wBAAqB,CAAC;AAI9F,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,UAAU,CAA4C;gBAElD,OAAO,EAAE,2BAA2B;IAQhD;;;;;OAKG;IACH,iBAAiB,IAAI,IAAI;IAKzB,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,mEAAmE;IACnE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C;;;;;;OAMG;IACG,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"hosted-flow-controller.d.ts","sourceRoot":"","sources":["../../../../../src/core/services/hosted-flow-controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,eAAe,EAAqC,MAAM,wBAAqB,CAAC;AAI9F,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,UAAU,CAA4C;gBAElD,OAAO,EAAE,2BAA2B;IAQhD;;;;;OAKG;IACH,iBAAiB,IAAI,IAAI;IAKzB,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,mEAAmE;IACnE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C;;;;;;OAMG;IACG,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAiDzD;;;;;OAKG;IACH,+EAA+E;IAC/E,OAAO,CAAC,eAAe;IAKvB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI;IA4CrC;;;;;;;;;;;;;OAaG;IACH,UAAU,IAAI,IAAI;IA2BlB;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAWb;;;;;OAKG;IACH,MAAM,IAAI,IAAI;IAed;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAmBf,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,wBAAwB;IAMhC;;;;;;;OAOG;YACW,QAAQ;IAiCtB,OAAO,CAAC,MAAM;CAcf"}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The page posts to `window.ReactNativeWebView.postMessage` as a JSON string,
|
|
9
9
|
* which arrives here as `event.nativeEvent.data`.
|
|
10
10
|
*/
|
|
11
|
-
export type HostedBridgeMessage = HostedReady | HostedOpenUrl | HostedCancel | HostedError;
|
|
11
|
+
export type HostedBridgeMessage = HostedReady | HostedOpenUrl | HostedCancel | HostedError | HostedTelemetryRequest;
|
|
12
12
|
/** The payable page is interactive. */
|
|
13
13
|
export interface HostedReady {
|
|
14
14
|
readonly type: 'ready';
|
|
@@ -33,6 +33,17 @@ export interface HostedError {
|
|
|
33
33
|
readonly type: 'error';
|
|
34
34
|
readonly code: string;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The page is asking for this device's native risk facts.
|
|
38
|
+
*
|
|
39
|
+
* The one message that expects an answer — not over this channel, which cannot
|
|
40
|
+
* reply, but through the `window.__spareLinkNativeTelemetry` callback the page
|
|
41
|
+
* exposes and the shell invokes by injecting script. A shell that stays silent
|
|
42
|
+
* costs the page one bounded timeout and nothing else.
|
|
43
|
+
*/
|
|
44
|
+
export interface HostedTelemetryRequest {
|
|
45
|
+
readonly type: 'telemetryRequest';
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Parses a message posted by the hosted page.
|
|
38
49
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-message.d.ts","sourceRoot":"","sources":["../../../../../src/internal/hosted/bridge-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"bridge-message.d.ts","sourceRoot":"","sources":["../../../../../src/internal/hosted/bridge-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,aAAa,GACb,YAAY,GACZ,WAAW,GACX,sBAAsB,CAAC;AAE3B,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA2BjF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivers a telemetry reply into the hosted page.
|
|
3
|
+
*
|
|
4
|
+
* The WebView message channel is one-way, so the reply travels the other way:
|
|
5
|
+
* the page exposes `window.__spareLinkNativeTelemetry` before it asks, and the
|
|
6
|
+
* shell answers by injecting a call to it. A page without the callback — an
|
|
7
|
+
* older page, or one that already gave up waiting — receives nothing and the
|
|
8
|
+
* injected chunk evaluates to a harmless literal.
|
|
9
|
+
*/
|
|
10
|
+
/** Page-exposed global the shell invokes with its telemetry reply. */
|
|
11
|
+
export declare const NATIVE_TELEMETRY_CALLBACK_NAME = "__spareLinkNativeTelemetry";
|
|
12
|
+
/**
|
|
13
|
+
* Builds the script that hands [payload] to the page's telemetry callback.
|
|
14
|
+
*
|
|
15
|
+
* The payload is embedded as a JSON literal. U+2028 and U+2029 are legal inside
|
|
16
|
+
* a JSON string but terminate a statement when evaluated as script, so they are
|
|
17
|
+
* escaped; the result is a single expression that is inert on a page without
|
|
18
|
+
* the callback.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildNativeTelemetryReplyScript(payload: Record<string, unknown>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Builds the handler the webview runs when the page asks for telemetry.
|
|
23
|
+
*
|
|
24
|
+
* Collection and injection are injected so the handler is a pure seam: the
|
|
25
|
+
* component wires the real collector and the WebView reference into it. A
|
|
26
|
+
* collector that answers nothing, or throws, injects nothing — the page's own
|
|
27
|
+
* timeout settles the request.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createTelemetryRequestHandler(deps: {
|
|
30
|
+
collect: () => Promise<Record<string, unknown> | null>;
|
|
31
|
+
inject: (script: string) => void;
|
|
32
|
+
}): () => Promise<void>;
|
|
33
|
+
//# sourceMappingURL=native-telemetry-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-telemetry-reply.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/native-telemetry-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQxF;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACvD,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAgBtB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects this device's native risk facts for the hosted payment page.
|
|
3
|
+
*
|
|
4
|
+
* Every probe is injected, so the collector runs under Jest with fakes and the
|
|
5
|
+
* module wiring lives in `risk-telemetry-probes`. Every probe is also allowed to
|
|
6
|
+
* fail or hang: a throwing or slow reading leaves its fact absent, because a risk
|
|
7
|
+
* reading must never delay or break a payment.
|
|
8
|
+
*
|
|
9
|
+
* The payload mirrors the page's own risk contract: camelCase keys under a
|
|
10
|
+
* `deviceInformation` block, facts only, nothing fabricated.
|
|
11
|
+
*/
|
|
12
|
+
/** How long any single probe may take before its fact is given up on. */
|
|
13
|
+
export declare const TELEMETRY_PROBE_BUDGET_MS = 800;
|
|
14
|
+
/** Identity and build facts read from the platform. */
|
|
15
|
+
export interface RiskDeviceSnapshot {
|
|
16
|
+
readonly operatingSystem: string | null;
|
|
17
|
+
readonly operatingSystemVersion: string | null;
|
|
18
|
+
readonly model: string | null;
|
|
19
|
+
readonly manufacturer: string | null;
|
|
20
|
+
/** A per-vendor install-stable identifier, when the platform offers one. */
|
|
21
|
+
readonly vendorIdentifier: string | null;
|
|
22
|
+
/** False on simulators and emulators. */
|
|
23
|
+
readonly isPhysicalDevice: boolean | null;
|
|
24
|
+
}
|
|
25
|
+
/** A battery reading, with the level as a 0 to 1 fraction. */
|
|
26
|
+
export interface RiskBatteryReading {
|
|
27
|
+
readonly levelFraction: number | null;
|
|
28
|
+
readonly isCharging: boolean | null;
|
|
29
|
+
}
|
|
30
|
+
/** The network medium classes the risk payload distinguishes. */
|
|
31
|
+
export type RiskConnectionKind = 'wifi' | 'cellular' | 'other';
|
|
32
|
+
/** The probes the collector runs. Each may answer null, throw, or hang. */
|
|
33
|
+
export interface RiskTelemetryProbes {
|
|
34
|
+
loadDeviceSnapshot(): Promise<RiskDeviceSnapshot | null>;
|
|
35
|
+
loadBattery(): Promise<RiskBatteryReading | null>;
|
|
36
|
+
loadConnection(): Promise<RiskConnectionKind | null>;
|
|
37
|
+
localeTag(): string | null;
|
|
38
|
+
localDateTime(): string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collects whatever the device answered within the per-probe budget.
|
|
42
|
+
*
|
|
43
|
+
* Returns null when nothing answered, so a caller hands the page nothing rather
|
|
44
|
+
* than an empty block.
|
|
45
|
+
*/
|
|
46
|
+
export declare function collectRiskTelemetry(probes: RiskTelemetryProbes): Promise<Record<string, unknown> | null>;
|
|
47
|
+
//# sourceMappingURL=risk-telemetry-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk-telemetry-collector.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/risk-telemetry-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,uDAAuD;AACvD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,yCAAyC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3C;AAED,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAED,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;AAE/D,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACzD,WAAW,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAClD,cAAc,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,IAAI,MAAM,GAAG,IAAI,CAAC;CAChC;AA4BD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAgDzC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real device probes behind `collectRiskTelemetry`.
|
|
3
|
+
*
|
|
4
|
+
* Every expo module here is an optional peer, loaded lazily inside a try/catch:
|
|
5
|
+
* a host app that ships without one simply reports that fact as absent, and no
|
|
6
|
+
* import of this file can throw for want of a native module. Structural `*Like`
|
|
7
|
+
* types keep this file honest about how little of each module it relies on.
|
|
8
|
+
*/
|
|
9
|
+
import type { RiskTelemetryProbes } from './risk-telemetry-collector.js';
|
|
10
|
+
/**
|
|
11
|
+
* Formats a date as local-time ISO 8601 with a numeric UTC offset, e.g.
|
|
12
|
+
* `2026-08-25T10:45:46.319+05:30` — never the UTC `Z` form. This keeps the
|
|
13
|
+
* timestamp on the payer's own clock so it reads the same as the device shows,
|
|
14
|
+
* and matches the Flutter SDK's `deviceLocalDateTime`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatLocalIsoWithOffset(date: Date): string;
|
|
17
|
+
/** The production probe set, ready for `collectRiskTelemetry`. */
|
|
18
|
+
export declare function createRiskTelemetryProbes(): RiskTelemetryProbes;
|
|
19
|
+
//# sourceMappingURL=risk-telemetry-probes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk-telemetry-probes.d.ts","sourceRoot":"","sources":["../../../../../src/internal/telemetry/risk-telemetry-probes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAIV,mBAAmB,EACpB,MAAM,+BAA4B,CAAC;AA8GpC;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAc3D;AAUD,kEAAkE;AAClE,wBAAgB,yBAAyB,IAAI,mBAAmB,CAE/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-payment-page.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+CAA4C,CAAC;
|
|
1
|
+
{"version":3,"file":"hosted-payment-page.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+CAA4C,CAAC;AAevF,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,oBAAoB,CAAC;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,CA6I5E"}
|
|
@@ -22,6 +22,15 @@ export interface HostedPaymentWebViewProps {
|
|
|
22
22
|
* this component stays a transport.
|
|
23
23
|
*/
|
|
24
24
|
onBridgeMessage: (message: unknown) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Answers the page's telemetry request with this device's native risk facts.
|
|
27
|
+
*
|
|
28
|
+
* The reply travels back by injected script rather than a return value, so it
|
|
29
|
+
* is handled here where the webview reference lives, and the message never
|
|
30
|
+
* reaches [onBridgeMessage]. Absent, or answering null, means the page
|
|
31
|
+
* proceeds with what the browser alone can see.
|
|
32
|
+
*/
|
|
33
|
+
onTelemetryRequest?: () => Promise<Record<string, unknown> | null>;
|
|
25
34
|
/** Called when the page itself could not be loaded. */
|
|
26
35
|
onLoadFailure?: () => void;
|
|
27
36
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-payment-webview.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-webview.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hosted-payment-webview.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hosted/hosted-payment-webview.tsx"],"names":[],"mappings":"AAwBA;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE3F;AAED,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,GAAG,CAAC,OAAO,CAuHlF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spare-technologies/spare-link",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "React Native SDK for Spare Link — open-banking payments for iOS and Android.",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -83,18 +83,34 @@
|
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@expo/vector-icons": "~14.0.0",
|
|
85
85
|
"expo": ">=52.0.0 <53.0.0",
|
|
86
|
+
"expo-application": "~6.0.0",
|
|
87
|
+
"expo-battery": "~9.0.0",
|
|
88
|
+
"expo-device": "~7.0.0",
|
|
86
89
|
"expo-font": "~13.0.0",
|
|
87
90
|
"expo-linear-gradient": "~14.0.0",
|
|
91
|
+
"expo-network": "~7.0.0",
|
|
88
92
|
"expo-secure-store": "~14.0.0",
|
|
89
93
|
"expo-web-browser": "~14.0.0",
|
|
90
94
|
"react": ">=18.3.0 <19.0.0",
|
|
91
95
|
"react-native": ">=0.76.0 <0.77.0",
|
|
92
96
|
"react-native-safe-area-context": "~4.12.0",
|
|
93
|
-
"react-native-webview": "~13.12.5",
|
|
94
97
|
"react-native-screens": "~4.4.0",
|
|
95
|
-
"react-native-svg": "^15.8.0"
|
|
98
|
+
"react-native-svg": "^15.8.0",
|
|
99
|
+
"react-native-webview": "~13.12.5"
|
|
96
100
|
},
|
|
97
101
|
"peerDependenciesMeta": {
|
|
102
|
+
"expo-application": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"expo-battery": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"expo-device": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"expo-network": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
98
114
|
"react-native-svg": {
|
|
99
115
|
"optional": true
|
|
100
116
|
}
|
|
@@ -106,9 +122,13 @@
|
|
|
106
122
|
"@types/react": "~18.3.0",
|
|
107
123
|
"del-cli": "^7.0.0",
|
|
108
124
|
"expo": "~52.0.0",
|
|
125
|
+
"expo-application": "~6.0.2",
|
|
126
|
+
"expo-battery": "~9.0.2",
|
|
127
|
+
"expo-device": "~7.0.3",
|
|
109
128
|
"expo-font": "~13.0.0",
|
|
110
129
|
"expo-linear-gradient": "~14.0.0",
|
|
111
130
|
"expo-linking": "~7.0.0",
|
|
131
|
+
"expo-network": "~7.0.5",
|
|
112
132
|
"expo-secure-store": "~14.0.0",
|
|
113
133
|
"expo-web-browser": "~14.0.0",
|
|
114
134
|
"jest": "^29.7.0",
|
|
@@ -117,8 +137,8 @@
|
|
|
117
137
|
"react-native": "0.76.9",
|
|
118
138
|
"react-native-builder-bob": "^0.43.0",
|
|
119
139
|
"react-native-safe-area-context": "~4.12.0",
|
|
120
|
-
"react-native-webview": "~13.12.5",
|
|
121
140
|
"react-native-screens": "~4.4.0",
|
|
141
|
+
"react-native-webview": "~13.12.5",
|
|
122
142
|
"react-test-renderer": "18.3.1",
|
|
123
143
|
"typescript": "^5.6.2"
|
|
124
144
|
}
|
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
* ]
|
|
12
12
|
* ```
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* This registers the deep link the payer returns through, and declares the
|
|
15
|
+
* device data the SDK collects in the app's iOS privacy manifest. The SDK still
|
|
16
|
+
* asks for no runtime permissions of its own — the optional risk-telemetry
|
|
17
|
+
* probes read only what the platform exposes to any app, and the payment page's
|
|
18
|
+
* coarse-location fix is gated by a permission the host app chooses to hold — but
|
|
19
|
+
* that data still has to be disclosed for App Store review, so the collected-data
|
|
20
|
+
* types are merged into `ios.privacyManifests` here rather than left to each
|
|
21
|
+
* integrator to rediscover.
|
|
17
22
|
*
|
|
18
23
|
* Written as plain CJS so the Expo CLI's plugin loader (which `require()`s
|
|
19
24
|
* this file) works regardless of whether Node loads it as ESM or CJS.
|
|
@@ -25,15 +30,83 @@
|
|
|
25
30
|
|
|
26
31
|
const { withAndroidManifest, withInfoPlist } = require('@expo/config-plugins');
|
|
27
32
|
|
|
33
|
+
const APP_FUNCTIONALITY = 'NSPrivacyCollectedDataTypePurposeAppFunctionality';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The data types this SDK is responsible for collecting on iOS, for the app's
|
|
37
|
+
* `PrivacyInfo.xcprivacy`. Both are unlinked (not tied to a user identity by the
|
|
38
|
+
* SDK) and never used for tracking; the purpose is fraud prevention, which Apple
|
|
39
|
+
* files under App Functionality.
|
|
40
|
+
*
|
|
41
|
+
* - Device ID: the iOS vendor identifier (IDFV), read by the risk-telemetry
|
|
42
|
+
* probe via `expo-application` when that optional peer is installed.
|
|
43
|
+
* - Coarse location: the payment page may attach an approximate fix as a risk
|
|
44
|
+
* signal, gated by a location permission the host app holds.
|
|
45
|
+
*/
|
|
46
|
+
const SPARE_COLLECTED_DATA_TYPES = [
|
|
47
|
+
{
|
|
48
|
+
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeDeviceID',
|
|
49
|
+
NSPrivacyCollectedDataTypeLinked: false,
|
|
50
|
+
NSPrivacyCollectedDataTypeTracking: false,
|
|
51
|
+
NSPrivacyCollectedDataTypePurposes: [APP_FUNCTIONALITY],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeCoarseLocation',
|
|
55
|
+
NSPrivacyCollectedDataTypeLinked: false,
|
|
56
|
+
NSPrivacyCollectedDataTypeTracking: false,
|
|
57
|
+
NSPrivacyCollectedDataTypePurposes: [APP_FUNCTIONALITY],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merges the SDK's collected-data declarations into `ios.privacyManifests`,
|
|
63
|
+
* which Expo's own prebuild step writes into `PrivacyInfo.xcprivacy`.
|
|
64
|
+
*
|
|
65
|
+
* Additive and idempotent: a data type the app already declares is left as the
|
|
66
|
+
* app set it, ours are added only when absent, and a second pass changes
|
|
67
|
+
* nothing. `NSPrivacyTracking` is set to false only when the app has not already
|
|
68
|
+
* made its own (possibly true) declaration.
|
|
69
|
+
*
|
|
70
|
+
* @param {import('@expo/config-plugins').ExpoConfig} config
|
|
71
|
+
*/
|
|
72
|
+
function withRiskTelemetryPrivacyManifest(config) {
|
|
73
|
+
if (!config.ios) {
|
|
74
|
+
config.ios = {};
|
|
75
|
+
}
|
|
76
|
+
if (!config.ios.privacyManifests) {
|
|
77
|
+
config.ios.privacyManifests = {};
|
|
78
|
+
}
|
|
79
|
+
const manifests = config.ios.privacyManifests;
|
|
80
|
+
|
|
81
|
+
if (manifests.NSPrivacyTracking === undefined) {
|
|
82
|
+
manifests.NSPrivacyTracking = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!manifests.NSPrivacyCollectedDataTypes) {
|
|
86
|
+
manifests.NSPrivacyCollectedDataTypes = [];
|
|
87
|
+
}
|
|
88
|
+
const declared = manifests.NSPrivacyCollectedDataTypes;
|
|
89
|
+
for (const dataType of SPARE_COLLECTED_DATA_TYPES) {
|
|
90
|
+
const already = declared.some(
|
|
91
|
+
(entry) => entry.NSPrivacyCollectedDataType === dataType.NSPrivacyCollectedDataType,
|
|
92
|
+
);
|
|
93
|
+
if (!already) {
|
|
94
|
+
declared.push(dataType);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
28
101
|
/**
|
|
29
102
|
* @param {import('@expo/config-plugins').ExpoConfig} config
|
|
30
103
|
* @param {SpareLinkPluginOptions | undefined} [props]
|
|
31
104
|
*/
|
|
32
105
|
function withSpareLink(config, props) {
|
|
33
106
|
const opts = props ?? {};
|
|
34
|
-
let next = config;
|
|
107
|
+
let next = withRiskTelemetryPrivacyManifest(config);
|
|
35
108
|
|
|
36
|
-
// iOS — the return URL scheme
|
|
109
|
+
// iOS — the return URL scheme.
|
|
37
110
|
next = withInfoPlist(next, (cfg) => {
|
|
38
111
|
if (opts.merchantScheme) {
|
|
39
112
|
const types = cfg.modResults.CFBundleURLTypes ?? [];
|
|
@@ -130,6 +130,11 @@ export class HostedFlowController {
|
|
|
130
130
|
}),
|
|
131
131
|
);
|
|
132
132
|
break;
|
|
133
|
+
case 'telemetryRequest':
|
|
134
|
+
// Answered by the webview layer, which owns the injection channel the
|
|
135
|
+
// reply travels back on. Reaching here means no telemetry handler was
|
|
136
|
+
// wired, and the page's own timeout settles the request.
|
|
137
|
+
break;
|
|
133
138
|
}
|
|
134
139
|
return true;
|
|
135
140
|
}
|
|
@@ -10,7 +10,12 @@ import { parseUrlParts } from '../url-parts';
|
|
|
10
10
|
* The page posts to `window.ReactNativeWebView.postMessage` as a JSON string,
|
|
11
11
|
* which arrives here as `event.nativeEvent.data`.
|
|
12
12
|
*/
|
|
13
|
-
export type HostedBridgeMessage =
|
|
13
|
+
export type HostedBridgeMessage =
|
|
14
|
+
| HostedReady
|
|
15
|
+
| HostedOpenUrl
|
|
16
|
+
| HostedCancel
|
|
17
|
+
| HostedError
|
|
18
|
+
| HostedTelemetryRequest;
|
|
14
19
|
|
|
15
20
|
/** The payable page is interactive. */
|
|
16
21
|
export interface HostedReady {
|
|
@@ -40,6 +45,18 @@ export interface HostedError {
|
|
|
40
45
|
readonly code: string;
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The page is asking for this device's native risk facts.
|
|
50
|
+
*
|
|
51
|
+
* The one message that expects an answer — not over this channel, which cannot
|
|
52
|
+
* reply, but through the `window.__spareLinkNativeTelemetry` callback the page
|
|
53
|
+
* exposes and the shell invokes by injecting script. A shell that stays silent
|
|
54
|
+
* costs the page one bounded timeout and nothing else.
|
|
55
|
+
*/
|
|
56
|
+
export interface HostedTelemetryRequest {
|
|
57
|
+
readonly type: 'telemetryRequest';
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
/**
|
|
44
61
|
* Parses a message posted by the hosted page.
|
|
45
62
|
*
|
|
@@ -64,6 +81,8 @@ export function parseHostedBridgeMessage(raw: unknown): HostedBridgeMessage | nu
|
|
|
64
81
|
}
|
|
65
82
|
case 'spare.cancel':
|
|
66
83
|
return { type: 'cancel' };
|
|
84
|
+
case 'spare.telemetry_request':
|
|
85
|
+
return { type: 'telemetryRequest' };
|
|
67
86
|
case 'spare.error': {
|
|
68
87
|
const code = message.code;
|
|
69
88
|
if (typeof code !== 'string' || code.trim().length === 0) return null;
|