@fingerprint79/react-native 0.0.1

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.
Files changed (52) hide show
  1. package/README.md +145 -0
  2. package/android/build.gradle +59 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/com/fingerprint79/rn/FpRnModule.kt +121 -0
  5. package/android/src/main/java/com/fingerprint79/rn/FpRnPackage.kt +19 -0
  6. package/dist/.tsbuildinfo-build +1 -0
  7. package/dist/client.d.ts +13 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/context.d.ts +17 -0
  10. package/dist/context.d.ts.map +1 -0
  11. package/dist/hooks.d.ts +27 -0
  12. package/dist/hooks.d.ts.map +1 -0
  13. package/dist/index.cjs +3717 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.ts +20 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.mjs +3717 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/native.d.ts +9 -0
  20. package/dist/native.d.ts.map +1 -0
  21. package/dist/signals/hardware.d.ts +11 -0
  22. package/dist/signals/hardware.d.ts.map +1 -0
  23. package/dist/signals/index.d.ts +3 -0
  24. package/dist/signals/index.d.ts.map +1 -0
  25. package/dist/signals/intl.d.ts +5 -0
  26. package/dist/signals/intl.d.ts.map +1 -0
  27. package/dist/signals/mobile.d.ts +24 -0
  28. package/dist/signals/mobile.d.ts.map +1 -0
  29. package/dist/signals/network.d.ts +5 -0
  30. package/dist/signals/network.d.ts.map +1 -0
  31. package/dist/signals/persistence.d.ts +17 -0
  32. package/dist/signals/persistence.d.ts.map +1 -0
  33. package/dist/types.d.ts +50 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/fingerprint79-react-native.podspec +26 -0
  36. package/ios/FpRnModule.m +16 -0
  37. package/ios/FpRnModule.swift +134 -0
  38. package/package.json +91 -0
  39. package/react-native.config.js +18 -0
  40. package/src/client.ts +89 -0
  41. package/src/context.tsx +82 -0
  42. package/src/hooks.ts +116 -0
  43. package/src/index.ts +20 -0
  44. package/src/native.ts +39 -0
  45. package/src/signals/hardware.ts +36 -0
  46. package/src/signals/index.ts +44 -0
  47. package/src/signals/intl.ts +63 -0
  48. package/src/signals/mobile.ts +23 -0
  49. package/src/signals/network.ts +51 -0
  50. package/src/signals/persistence.ts +77 -0
  51. package/src/signals/signals.test.ts +111 -0
  52. package/src/types.ts +54 -0
@@ -0,0 +1,9 @@
1
+ import type { MobileNativePayload } from './signals/mobile.js';
2
+ /** True when the native module is actually linked. Drives a one-time
3
+ * warning the FpProvider emits in dev mode if missing. */
4
+ export declare function isNativeLinked(): boolean;
5
+ /** Collect mobile signals from the linked native module. Returns an empty
6
+ * object if the module isn't linked — the rest of the pipeline handles
7
+ * the absence by simply not emitting `signals.mobile`. */
8
+ export declare function collectNativeMobile(): Promise<MobileNativePayload>;
9
+ //# sourceMappingURL=native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAU/D;2DAC2D;AAC3D,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;2DAE2D;AAC3D,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAWxE"}
@@ -0,0 +1,11 @@
1
+ import type { ClientSignals } from '@fp/shared-types';
2
+ type HardwareBlock = NonNullable<ClientSignals['hardware']>;
3
+ /**
4
+ * Maps RN runtime info into the wire schema's `hardware` block. Browser
5
+ * fields that don't exist on mobile (oscpu, devicePixelRatio's CSS
6
+ * meaning, colorGamut from media query, ...) stay absent — Zod treats
7
+ * every leaf as optional.
8
+ */
9
+ export declare function collectHardware(): HardwareBlock;
10
+ export {};
11
+ //# sourceMappingURL=hardware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardware.d.ts","sourceRoot":"","sources":["../../src/signals/hardware.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,KAAK,aAAa,GAAG,WAAW,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,aAAa,CAmB/C"}
@@ -0,0 +1,3 @@
1
+ import type { ClientSignals } from '@fp/shared-types';
2
+ export declare function collectSignals(): Promise<ClientSignals>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signals/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAW7D"}
@@ -0,0 +1,5 @@
1
+ import type { ClientSignals } from '@fp/shared-types';
2
+ type IntlBlock = NonNullable<ClientSignals['intl']>;
3
+ export declare function collectIntl(): IntlBlock;
4
+ export {};
5
+ //# sourceMappingURL=intl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intl.d.ts","sourceRoot":"","sources":["../../src/signals/intl.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,KAAK,SAAS,GAAG,WAAW,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAiBpD,wBAAgB,WAAW,IAAI,SAAS,CAUvC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shape of what the native bridge returns. Kept separate from the wire
3
+ * schema so the bridge can evolve without forcing a `@fp/shared-types`
4
+ * bump on every native change — anything new the native side starts
5
+ * sending is just ignored by Zod's `.strict()` until we plumb it
6
+ * through here.
7
+ *
8
+ * Field names mirror `ClientSignalsSchema.mobile` exactly so the only
9
+ * step the client does is `signals.mobile = { ...native }`.
10
+ */
11
+ export interface MobileNativePayload {
12
+ os?: 'ios' | 'android';
13
+ osVersion?: string;
14
+ model?: string;
15
+ manufacturer?: string;
16
+ idfv?: string;
17
+ androidId?: string;
18
+ installId?: string;
19
+ totalMemoryMb?: number;
20
+ isTablet?: boolean;
21
+ bundleId?: string;
22
+ appVersion?: string;
23
+ }
24
+ //# sourceMappingURL=mobile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile.d.ts","sourceRoot":"","sources":["../../src/signals/mobile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,5 @@
1
+ import type { ClientSignals } from '@fp/shared-types';
2
+ type NetworkBlock = NonNullable<ClientSignals['network']>;
3
+ export declare function collectNetwork(): Promise<NetworkBlock | undefined>;
4
+ export {};
5
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/signals/network.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,KAAK,YAAY,GAAG,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;AA0B1D,wBAAsB,cAAc,IAAI,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAmBxE"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Synchronous session-id accessor — returns whatever the SDK has in
3
+ * memory right now. On first SDK boot the restore path may not have
4
+ * finished yet, in which case we mint a new id immediately and overwrite
5
+ * with the persisted value if/when restore lands (we don't, because
6
+ * payloads have already gone out with the fresh id). Mirrors the
7
+ * trade-off FP Pro RN makes — the moment of app-cold-boot is the one
8
+ * weak spot in mobile session-id continuity.
9
+ */
10
+ export declare function getOrCreateSessionId(): string;
11
+ /**
12
+ * Best-effort hydrate from AsyncStorage. Call once at SDK boot; if it
13
+ * lands before the first `identify()` we use the persisted id, otherwise
14
+ * we keep the fresh one.
15
+ */
16
+ export declare function restoreSessionId(): Promise<void>;
17
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/signals/persistence.ts"],"names":[],"mappings":"AAwCA;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAO7C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAahD"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Public TypeScript surface — mirrors `apps/sdk-react/src/types.ts` so
3
+ * code that switches between web and RN sees the same option names.
4
+ *
5
+ * The mobile-specific bits (collected through the native bridge) are NOT
6
+ * exposed here — they're an implementation detail of the payload, never
7
+ * a knob the integrator turns. The schema lives in
8
+ * `@fp/shared-types/ingest.ts::ClientSignalsSchema.mobile`.
9
+ */
10
+ export type FpEventType = 'pageview' | 'login' | 'signup' | 'action' | 'heartbeat';
11
+ export interface FpConfig {
12
+ /** Origin where the platform's collector is reachable (e.g.
13
+ * `https://yoursite.com/fpjs` proxied via a Cloudflare Worker). */
14
+ collectorUrl: string;
15
+ /** Tenant key from the dashboard's API Keys page. Forwarded as
16
+ * `X-Project-Key` so the collector routes events to the right project. */
17
+ projectKey: string;
18
+ /** Pre-shared X25519 public key (base64) — skips the bootstrap /v1/pk
19
+ * hop. Most consumers leave this undefined and let `ServerPublicKey\
20
+ * Cache` resolve it on first identify(). */
21
+ serverPublicKeyB64?: string;
22
+ /** Override the SDK version reported in the payload. */
23
+ sdkVersion?: string;
24
+ }
25
+ export interface IdentifyOptions {
26
+ event?: FpEventType;
27
+ /** Opaque integrator-facing user id. Surfaces as «Linked ID» in the
28
+ * dashboard's Identification table. */
29
+ userId?: string;
30
+ /** Block the HTTP response until scoring finishes (~50-150 ms) so the
31
+ * result includes the real `visitorId` / `suspectScore`. Use for
32
+ * click-driven flows. Defaults to `true`. */
33
+ sync?: boolean;
34
+ }
35
+ export interface IdentifyResult {
36
+ v?: string;
37
+ eventId?: string;
38
+ result?: 'ok' | 'retry' | 'reject';
39
+ visitorId?: string;
40
+ userId?: string;
41
+ suspectScore?: number;
42
+ external?: {
43
+ flags?: string[];
44
+ linkedVisitors?: string[];
45
+ };
46
+ }
47
+ export interface FpInstance {
48
+ identify: (opts?: IdentifyOptions) => Promise<IdentifyResult | null>;
49
+ }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEnF,MAAM,WAAW,QAAQ;IACvB;wEACoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB;+EAC2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB;;iDAE6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;4CACwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;kDAE8C;IAC9C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACtE"}
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ # Podspec for @fingerprint79/react-native. Picked up by RN >=0.60 autolinking
4
+ # via react-native.config.js — consumers don't edit this file.
5
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
6
+
7
+ Pod::Spec.new do |s|
8
+ s.name = 'fingerprint79-react-native'
9
+ s.version = package['version']
10
+ s.summary = package['description']
11
+ s.license = package['license']
12
+ s.homepage = package['homepage']
13
+ s.authors = { 'Fingerprint Platform contributors' => 'noreply@example.com' }
14
+ s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
15
+
16
+ # Mirrors FP Pro's minimum: iOS 12 (Swift 5.7+ is implied by Xcode 14).
17
+ s.platforms = { :ios => '12.0' }
18
+ s.swift_versions = ['5.7', '5.8', '5.9']
19
+
20
+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
21
+ s.requires_arc = true
22
+
23
+ # RN core — every native module pulls this in. Autolinking ensures the
24
+ # consumer's React-Core pod resolves at the right version.
25
+ s.dependency 'React-Core'
26
+ end
@@ -0,0 +1,16 @@
1
+ // Obj-C bridge header — what RN_EXTERN_MODULE registers Swift classes as
2
+ // when the host app is still using the old bridge (RN <0.74). Auto-loaded
3
+ // by autolinking; the file's mere presence is all RN needs.
4
+ #import <React/RCTBridgeModule.h>
5
+
6
+ @interface RCT_EXTERN_MODULE(FpRnModule, NSObject)
7
+
8
+ RCT_EXTERN_METHOD(collect:(RCTPromiseResolveBlock)resolve
9
+ reject:(RCTPromiseRejectBlock)reject)
10
+
11
+ // `+requiresMainQueueSetup` is defined in Swift; declaring it here lets
12
+ // RN's bridge see the override at registration time on old RN versions
13
+ // that still introspect Obj-C method lists.
14
+ + (BOOL)requiresMainQueueSetup;
15
+
16
+ @end
@@ -0,0 +1,134 @@
1
+ // FpRnModule.swift
2
+ //
3
+ // Native bridge for @fingerprint79/react-native on iOS. Exposes one method
4
+ // — `collect()` — that returns a dictionary the JS layer drops directly
5
+ // into `IngestPayload.signals.mobile`. Everything we collect here is
6
+ // either unreachable from JS (IDFV, Keychain UUID, sysctl model) or
7
+ // expensive to fetch reliably from RN's JS bridge (hardware.machine,
8
+ // physicalMemory).
9
+ //
10
+ // Identifier strategy mirrors what FingerprintJS Pro does:
11
+ // 1. `idfv` — UIDevice.identifierForVendor. Vendor-scoped UUID,
12
+ // stable until the user deletes every app from us.
13
+ // 2. `installId` — UUID we write to Keychain (kSecAttrAccessibleAfter\
14
+ // FirstUnlock). Survives app delete + reinstall when
15
+ // iCloud Keychain is enabled, which is the strongest
16
+ // Apple-sanctioned device-identifier we get without the
17
+ // AppTrackingTransparency prompt + IDFA.
18
+ //
19
+ // We do NOT touch IDFA / AppTrackingTransparency — that would force the
20
+ // host app into an ATT prompt and require an Info.plist string. Visitor
21
+ // continuity through Keychain is enough for the fraud-detection use case.
22
+
23
+ import Foundation
24
+ import UIKit
25
+
26
+ @objc(FpRnModule)
27
+ class FpRnModule: NSObject {
28
+
29
+ // MARK: - RN bridge plumbing
30
+
31
+ @objc static func requiresMainQueueSetup() -> Bool {
32
+ // Most of what we read is thread-safe (UIDevice / ProcessInfo / sysctl
33
+ // / Keychain). Returning false keeps us off the main thread and out
34
+ // of the RN startup critical path.
35
+ return false
36
+ }
37
+
38
+ @objc func methodQueue() -> DispatchQueue {
39
+ // Dedicated low-priority queue — `collect()` is called once per
40
+ // identify() so we don't need a thread per call.
41
+ return DispatchQueue(label: "com.fingerprint79.rn.collect", qos: .utility)
42
+ }
43
+
44
+ // MARK: - Public API
45
+
46
+ @objc(collect:reject:)
47
+ func collect(_ resolve: @escaping RCTPromiseResolveBlock,
48
+ reject: @escaping RCTPromiseRejectBlock) {
49
+ let device = UIDevice.current
50
+ let info = ProcessInfo.processInfo
51
+ let bundle = Bundle.main
52
+
53
+ // Memory in MiB. `physicalMemory` is bytes; integer-divide and clamp.
54
+ let memMib = Int(info.physicalMemory / (1024 * 1024))
55
+
56
+ let payload: [String: Any] = [
57
+ "os": "ios",
58
+ "osVersion": device.systemVersion,
59
+ "model": Self.hardwareModel(),
60
+ "manufacturer": "Apple",
61
+ "idfv": device.identifierForVendor?.uuidString as Any,
62
+ "installId": Self.keychainInstallId(),
63
+ "totalMemoryMb": memMib,
64
+ "isTablet": device.userInterfaceIdiom == .pad,
65
+ "bundleId": bundle.bundleIdentifier as Any,
66
+ "appVersion":
67
+ bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String as Any,
68
+ ]
69
+ resolve(payload)
70
+ }
71
+
72
+ // MARK: - Helpers
73
+
74
+ /// `sysctlbyname("hw.machine")` returns the model identifier
75
+ /// (`"iPhone15,2"`, `"iPad14,1"`). Marketing names are NOT exposed by
76
+ /// iOS — translation to "iPhone 14 Pro" happens server-side from a lookup
77
+ /// table the scoring worker holds.
78
+ private static func hardwareModel() -> String {
79
+ var size = 0
80
+ sysctlbyname("hw.machine", nil, &size, nil, 0)
81
+ var buf = [CChar](repeating: 0, count: size)
82
+ sysctlbyname("hw.machine", &buf, &size, nil, 0)
83
+ return String(cString: buf)
84
+ }
85
+
86
+ // Service identifier for Keychain. Globally unique to us — never collides
87
+ // with the host app's own Keychain items. AccessibleAfterFirstUnlock so
88
+ // the value survives device reboot but stays protected before the user's
89
+ // first PIN entry.
90
+ private static let kcService = "com.fingerprint79.installId"
91
+ private static let kcAccount = "default"
92
+
93
+ /// Returns a stable installation UUID, generating + persisting on first
94
+ /// call. Failures (Keychain locked, simulator quirks) fall back to a
95
+ /// fresh UUID so the call site never sees `nil` — at worst we lose
96
+ /// cross-launch continuity.
97
+ private static func keychainInstallId() -> String {
98
+ if let existing = readKeychain() { return existing }
99
+ let fresh = UUID().uuidString
100
+ writeKeychain(value: fresh)
101
+ return fresh
102
+ }
103
+
104
+ private static func readKeychain() -> String? {
105
+ let query: [String: Any] = [
106
+ kSecClass as String: kSecClassGenericPassword,
107
+ kSecAttrService as String: kcService,
108
+ kSecAttrAccount as String: kcAccount,
109
+ kSecReturnData as String: true,
110
+ kSecMatchLimit as String: kSecMatchLimitOne,
111
+ ]
112
+ var item: AnyObject?
113
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
114
+ guard status == errSecSuccess,
115
+ let data = item as? Data,
116
+ let str = String(data: data, encoding: .utf8) else { return nil }
117
+ return str
118
+ }
119
+
120
+ private static func writeKeychain(value: String) {
121
+ let data = value.data(using: .utf8)!
122
+ let attrs: [String: Any] = [
123
+ kSecClass as String: kSecClassGenericPassword,
124
+ kSecAttrService as String: kcService,
125
+ kSecAttrAccount as String: kcAccount,
126
+ kSecValueData as String: data,
127
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
128
+ ]
129
+ // Delete-then-add so SecItemAdd never trips on a stale duplicate (can
130
+ // happen after an OS upgrade migrated the keychain DB).
131
+ SecItemDelete(attrs as CFDictionary)
132
+ SecItemAdd(attrs as CFDictionary, nil)
133
+ }
134
+ }
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@fingerprint79/react-native",
3
+ "version": "0.0.1",
4
+ "description": "React Native binding for the Fingerprint Platform — JS hooks + native iOS (Swift) and Android (Kotlin) bridges. Collects mobile signals (IDFV, Keychain UUID, hardware) and ships sealed payloads to your self-hosted collector.",
5
+ "license": "MIT",
6
+ "author": "Fingerprint Platform contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "react-native": "./src/index.ts",
12
+ "source": "./src/index.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "react-native": "./src/index.ts",
17
+ "import": "./dist/index.mjs",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "ios",
26
+ "android",
27
+ "react-native.config.js",
28
+ "fingerprint79-react-native.podspec",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "sideEffects": false,
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/igorao79/fingerprint.git",
36
+ "directory": "apps/sdk-react-native"
37
+ },
38
+ "homepage": "https://github.com/igorao79/fingerprint/tree/main/apps/sdk-react-native#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/igorao79/fingerprint/issues"
41
+ },
42
+ "keywords": [
43
+ "fingerprint",
44
+ "react-native",
45
+ "ios",
46
+ "android",
47
+ "device-fingerprint",
48
+ "mobile-fingerprint",
49
+ "visitor-id",
50
+ "anti-fraud",
51
+ "bot-detection",
52
+ "fp-rn"
53
+ ],
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "vite build && tsc -p tsconfig.build.json",
59
+ "typecheck": "tsc -b --noEmit",
60
+ "lint": "eslint src",
61
+ "test": "vitest run",
62
+ "clean": "rimraf dist .turbo *.tsbuildinfo",
63
+ "prepublishOnly": "pnpm build"
64
+ },
65
+ "peerDependencies": {
66
+ "react": ">=18 <20",
67
+ "react-native": ">=0.72",
68
+ "react-native-localize": ">=3",
69
+ "@react-native-community/netinfo": ">=11",
70
+ "@react-native-async-storage/async-storage": ">=1.21"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "react-native-localize": { "optional": true },
74
+ "@react-native-community/netinfo": { "optional": true },
75
+ "@react-native-async-storage/async-storage": { "optional": true }
76
+ },
77
+ "dependencies": {},
78
+ "devDependencies": {
79
+ "@fp/crypto": "workspace:*",
80
+ "@fp/sdk-core": "workspace:*",
81
+ "@fp/shared-types": "workspace:*",
82
+ "@types/react": "^18.3.12",
83
+ "cbor-x": "^1.6.0",
84
+ "react": "^18.3.1",
85
+ "react-native": "^0.74.0",
86
+ "vite": "^5.4.11"
87
+ },
88
+ "engines": {
89
+ "node": ">=18"
90
+ }
91
+ }
@@ -0,0 +1,18 @@
1
+ // React Native autolinking manifest. RN >=0.60 reads this file in every
2
+ // installed dependency and wires the iOS pod + Android gradle module into
3
+ // the host app at `pod install` / `gradle assemble` time. Without this
4
+ // file the consumer would have to edit Podfile + settings.gradle by hand.
5
+ module.exports = {
6
+ dependency: {
7
+ platforms: {
8
+ ios: {
9
+ podspecPath: __dirname + '/fingerprint79-react-native.podspec',
10
+ },
11
+ android: {
12
+ sourceDir: __dirname + '/android',
13
+ packageImportPath: 'import com.fingerprint79.rn.FpRnPackage;',
14
+ packageInstance: 'new FpRnPackage()',
15
+ },
16
+ },
17
+ },
18
+ };
package/src/client.ts ADDED
@@ -0,0 +1,89 @@
1
+ // FpRnClient — RN counterpart of `FpSdk` in sdk-browser. Owns the
2
+ // public-key cache, runs collectSignals on every identify(), seals the
3
+ // CBOR-encoded payload with X25519+AES-GCM (same crypto as the browser
4
+ // SDK — payloads from web and mobile are interchangeable at the
5
+ // collector), and POSTs to /v1/ingest.
6
+
7
+ import { encode as cborEncode } from 'cbor-x';
8
+
9
+ import { ServerPublicKeyCache, sendIngest } from '@fp/sdk-core';
10
+ import { seal } from '@fp/crypto';
11
+
12
+ import { collectSignals } from './signals/index.js';
13
+ import { getOrCreateSessionId, restoreSessionId } from './signals/persistence.js';
14
+
15
+ import type { IngestPayload, IngestResponse } from '@fp/shared-types';
16
+ import type { FpConfig, IdentifyOptions } from './types.js';
17
+
18
+ // Vite's `define` inlines this at build time from package.json.
19
+ declare const __SDK_VERSION__: string;
20
+ const SDK_VERSION: string =
21
+ typeof __SDK_VERSION__ === 'string' ? __SDK_VERSION__ : '0.0.0-dev';
22
+
23
+ export class FpRnClient {
24
+ private readonly pkCache: ServerPublicKeyCache;
25
+ private readonly config: FpConfig;
26
+
27
+ constructor(config: FpConfig) {
28
+ this.config = config;
29
+ this.pkCache = new ServerPublicKeyCache(config.collectorUrl);
30
+ // Kick off persistence restore non-blocking. Late lands are fine —
31
+ // the first identify() may use a fresh id, all subsequent ones
32
+ // get the persisted one once AsyncStorage answers.
33
+ void restoreSessionId();
34
+ }
35
+
36
+ /** Warm-up the public-key cache. FpProvider awaits this in useEffect
37
+ * so the very first identify() call doesn't pay the /v1/pk hop and
38
+ * so auth misconfig surfaces as ready=false up-front. */
39
+ async warmUp(): Promise<void> {
40
+ await this.pkCache.getWithAlias();
41
+ }
42
+
43
+ async identify(opts: IdentifyOptions = {}): Promise<IngestResponse | null> {
44
+ const signals = await collectSignals();
45
+ const sessionId = getOrCreateSessionId();
46
+ const event = opts.event ?? 'pageview';
47
+ const syncMode = opts.sync ?? true;
48
+
49
+ const payload: IngestPayload = {
50
+ sdkVersion: SDK_VERSION,
51
+ ts: Date.now(),
52
+ sessionId,
53
+ // Mobile apps have no URL but the wire schema requires a string —
54
+ // use the bundle id (if collected via native) as a stable
55
+ // "namespace" so dashboard filters don't choke on every event
56
+ // sharing the same placeholder. Fall back to a sentinel scheme.
57
+ pageUrl: bundleAsUrl(signals.mobile?.bundleId) ?? 'rn://app',
58
+ referrer: '',
59
+ event,
60
+ signals,
61
+ };
62
+ if (opts.userId !== undefined) {
63
+ payload.accountHint = { userId: opts.userId };
64
+ }
65
+ if (syncMode) payload.sync = true;
66
+
67
+ const { key: serverKey, aliasPath } = await this.pkCache.getWithAlias();
68
+ const plaintext = new Uint8Array(cborEncode(payload));
69
+ const { wire } = await seal(serverKey.key, plaintext);
70
+
71
+ return await sendIngest({
72
+ collectorUrl: this.config.collectorUrl,
73
+ wire,
74
+ serverKeyId: serverKey.id,
75
+ sdkVersion: SDK_VERSION,
76
+ preferBeacon: false,
77
+ ...(aliasPath ? { aliasPath } : {}),
78
+ projectKey: this.config.projectKey,
79
+ });
80
+ }
81
+ }
82
+
83
+ /** `pageUrl` is `.url()`-validated by Zod. Bundle ids like `com.acme.app`
84
+ * aren't URLs, so wrap them as a custom scheme — keeps the dashboard
85
+ * filterable per-app without false validation errors. */
86
+ function bundleAsUrl(bundleId: string | undefined): string | undefined {
87
+ if (!bundleId) return undefined;
88
+ return `rn://${bundleId}`;
89
+ }
@@ -0,0 +1,82 @@
1
+ // FpProvider — RN mirror of `apps/sdk-react/src/context.tsx`. Unlike the
2
+ // web version we never wait for a global to appear; the SDK is JS code
3
+ // inside the same process, so we instantiate FpRnClient immediately and
4
+ // fire a no-op /v1/pk warm-up to surface auth errors early.
5
+
6
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
7
+
8
+ import { FpRnClient } from './client.js';
9
+ import { isNativeLinked } from './native.js';
10
+
11
+ import type { FpConfig } from './types.js';
12
+
13
+ interface FpProviderProps {
14
+ readonly config: FpConfig;
15
+ readonly children: ReactNode;
16
+ }
17
+
18
+ interface FpContextValue {
19
+ instance: FpRnClient | null;
20
+ ready: boolean;
21
+ error: Error | null;
22
+ }
23
+
24
+ const FpContext = createContext<FpContextValue>({
25
+ instance: null,
26
+ ready: false,
27
+ error: null,
28
+ });
29
+
30
+ export function FpProvider({ config, children }: FpProviderProps) {
31
+ // Stable instance for the provider's entire lifetime. Re-creating per
32
+ // render would discard the pk-fetcher's cache on every parent rerender.
33
+ const instance = useMemo(() => new FpRnClient(config), [config.collectorUrl, config.projectKey]);
34
+ const [state, setState] = useState<FpContextValue>({
35
+ instance: null,
36
+ ready: false,
37
+ error: null,
38
+ });
39
+
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ if (!isNativeLinked() && __DEV__) {
43
+ console.warn(
44
+ '[@fingerprint79/react-native] Native module not linked. ' +
45
+ 'Mobile signals (idfv / installId / hardware) will be absent. ' +
46
+ 'Did you run `pod install` (iOS) and rebuild (Android)?',
47
+ );
48
+ }
49
+ // Mark ready once the public-key fetch lands — that's the first
50
+ // request the SDK actually needs. Failures here surface as
51
+ // `ready: false, error: ...` so the UI can render a banner.
52
+ instance
53
+ .warmUp()
54
+ .then(() => {
55
+ if (cancelled) return;
56
+ setState({ instance, ready: true, error: null });
57
+ })
58
+ .catch((e: unknown) => {
59
+ if (cancelled) return;
60
+ setState({
61
+ instance: null,
62
+ ready: false,
63
+ error: e instanceof Error ? e : new Error(String(e)),
64
+ });
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [instance]);
70
+
71
+ return <FpContext.Provider value={state}>{children}</FpContext.Provider>;
72
+ }
73
+
74
+ /** Internal — every hook routes through this. */
75
+ export function useFpContext(): FpContextValue {
76
+ return useContext(FpContext);
77
+ }
78
+
79
+ // Module-level shim for `__DEV__` so this file typechecks outside Metro
80
+ // (which injects __DEV__ as a global). Library code is allowed to use
81
+ // it directly per RN convention.
82
+ declare const __DEV__: boolean;