deep-link-expo 1.0.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/CHANGELOG.md +70 -0
- package/LICENSE +22 -0
- package/README.md +138 -0
- package/dist/attribution/attribution-context.d.ts +34 -0
- package/dist/attribution/attribution-context.d.ts.map +1 -0
- package/dist/attribution/attribution-context.js +98 -0
- package/dist/attribution/attribution-manager.d.ts +18 -0
- package/dist/attribution/attribution-manager.d.ts.map +1 -0
- package/dist/attribution/attribution-manager.js +94 -0
- package/dist/deeplink/deep-link-handler.d.ts +43 -0
- package/dist/deeplink/deep-link-handler.d.ts.map +1 -0
- package/dist/deeplink/deep-link-handler.js +150 -0
- package/dist/deeplink/url-parser.d.ts +13 -0
- package/dist/deeplink/url-parser.d.ts.map +1 -0
- package/dist/deeplink/url-parser.js +92 -0
- package/dist/errors/linkforty-error.d.ts +26 -0
- package/dist/errors/linkforty-error.d.ts.map +1 -0
- package/dist/errors/linkforty-error.js +50 -0
- package/dist/events/event-queue.d.ts +14 -0
- package/dist/events/event-queue.d.ts.map +1 -0
- package/dist/events/event-queue.js +68 -0
- package/dist/events/event-tracker.d.ts +18 -0
- package/dist/events/event-tracker.d.ts.map +1 -0
- package/dist/events/event-tracker.js +92 -0
- package/dist/fingerprint/fingerprint-collector.d.ts +8 -0
- package/dist/fingerprint/fingerprint-collector.d.ts.map +1 -0
- package/dist/fingerprint/fingerprint-collector.js +27 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/linkforty-sdk.d.ts +53 -0
- package/dist/linkforty-sdk.d.ts.map +1 -0
- package/dist/linkforty-sdk.js +255 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +18 -0
- package/dist/models/attribution.d.ts +28 -0
- package/dist/models/attribution.d.ts.map +1 -0
- package/dist/models/attribution.js +9 -0
- package/dist/models/config.d.ts +72 -0
- package/dist/models/config.d.ts.map +1 -0
- package/dist/models/config.js +26 -0
- package/dist/models/create-link-options.d.ts +15 -0
- package/dist/models/create-link-options.d.ts.map +1 -0
- package/dist/models/create-link-options.js +1 -0
- package/dist/models/create-link-result.d.ts +8 -0
- package/dist/models/create-link-result.d.ts.map +1 -0
- package/dist/models/create-link-result.js +1 -0
- package/dist/models/deep-link-data.d.ts +14 -0
- package/dist/models/deep-link-data.d.ts.map +1 -0
- package/dist/models/deep-link-data.js +1 -0
- package/dist/models/device-fingerprint.d.ts +13 -0
- package/dist/models/device-fingerprint.d.ts.map +1 -0
- package/dist/models/device-fingerprint.js +1 -0
- package/dist/models/event-request.d.ts +13 -0
- package/dist/models/event-request.d.ts.map +1 -0
- package/dist/models/event-request.js +1 -0
- package/dist/models/install-response.d.ts +9 -0
- package/dist/models/install-response.d.ts.map +1 -0
- package/dist/models/install-response.js +1 -0
- package/dist/models/utm-parameters.d.ts +8 -0
- package/dist/models/utm-parameters.d.ts.map +1 -0
- package/dist/models/utm-parameters.js +1 -0
- package/dist/navigation/navigation-tracker.d.ts +52 -0
- package/dist/navigation/navigation-tracker.d.ts.map +1 -0
- package/dist/navigation/navigation-tracker.js +153 -0
- package/dist/network/network-manager.d.ts +13 -0
- package/dist/network/network-manager.d.ts.map +1 -0
- package/dist/network/network-manager.js +90 -0
- package/dist/storage/storage-keys.d.ts +8 -0
- package/dist/storage/storage-keys.d.ts.map +1 -0
- package/dist/storage/storage-keys.js +7 -0
- package/dist/storage/storage-manager.d.ts +20 -0
- package/dist/storage/storage-manager.d.ts.map +1 -0
- package/dist/storage/storage-manager.js +46 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/llms.txt +392 -0
- package/package.json +83 -0
- package/src/attribution/attribution-context.ts +106 -0
- package/src/attribution/attribution-manager.ts +126 -0
- package/src/deeplink/deep-link-handler.ts +171 -0
- package/src/deeplink/url-parser.ts +96 -0
- package/src/errors/linkforty-error.ts +89 -0
- package/src/events/event-queue.ts +75 -0
- package/src/events/event-tracker.ts +118 -0
- package/src/fingerprint/fingerprint-collector.ts +35 -0
- package/src/index.ts +29 -0
- package/src/linkforty-sdk.ts +342 -0
- package/src/logger.ts +23 -0
- package/src/models/attribution.ts +29 -0
- package/src/models/config.ts +109 -0
- package/src/models/create-link-options.ts +15 -0
- package/src/models/create-link-result.ts +7 -0
- package/src/models/deep-link-data.ts +14 -0
- package/src/models/device-fingerprint.ts +12 -0
- package/src/models/event-request.ts +15 -0
- package/src/models/install-response.ts +9 -0
- package/src/models/utm-parameters.ts +7 -0
- package/src/navigation/navigation-tracker.ts +195 -0
- package/src/network/network-manager.ts +111 -0
- package/src/storage/storage-keys.ts +7 -0
- package/src/storage/storage-manager.ts +61 -0
- package/src/version.ts +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
## 1.6.0 (2026-06-10)
|
|
2
|
+
|
|
3
|
+
* feat(sdk): report SDK name + version (SIT-235 · Expo) (#5) ([554b45d](https://github.com/LinkForty/mobile-sdk-expo/commit/554b45d)), closes [#5](https://github.com/LinkForty/mobile-sdk-expo/issues/5)
|
|
4
|
+
|
|
5
|
+
## 1.5.0 (2026-06-10)
|
|
6
|
+
|
|
7
|
+
* feat: port last-click attribution + autoTrackNavigation to the Expo SDK (SIT-237) (#4) ([418c160](https://github.com/LinkForty/mobile-sdk-expo/commit/418c160)), closes [#4](https://github.com/LinkForty/mobile-sdk-expo/issues/4)
|
|
8
|
+
|
|
9
|
+
## 1.4.0 (2026-05-04)
|
|
10
|
+
|
|
11
|
+
* feat: add appToken config option for Cloud organic-install attribution (#1) ([8261ca4](https://github.com/LinkForty/mobile-sdk-expo/commit/8261ca4)), closes [#1](https://github.com/LinkForty/mobile-sdk-expo/issues/1) [LinkForty/cloud#76](https://github.com/LinkForty/cloud/issues/76)
|
|
12
|
+
* ci: drop flaky 'npm install -g npm@latest' self-upgrade step (#2) ([f1cebdd](https://github.com/LinkForty/mobile-sdk-expo/commit/f1cebdd)), closes [#2](https://github.com/LinkForty/mobile-sdk-expo/issues/2) [#1](https://github.com/LinkForty/mobile-sdk-expo/issues/1)
|
|
13
|
+
* update packages ([334885e](https://github.com/LinkForty/mobile-sdk-expo/commit/334885e))
|
|
14
|
+
|
|
15
|
+
## 1.3.0 (2026-04-01)
|
|
16
|
+
|
|
17
|
+
* feat: add webFallbackUrl to CreateLinkOptions ([d96c7ae](https://github.com/LinkForty/mobile-sdk-expo/commit/d96c7ae))
|
|
18
|
+
|
|
19
|
+
## 1.2.0 (2026-03-17)
|
|
20
|
+
|
|
21
|
+
* ci: add semantic-release with OIDC trusted publishing to npm ([05c7918](https://github.com/LinkForty/mobile-sdk-expo/commit/05c7918))
|
|
22
|
+
* feat: add setExternalUserId for SDK-level user attribution ([f555707](https://github.com/LinkForty/mobile-sdk-expo/commit/f555707))
|
|
23
|
+
* chore: add .claude/ to gitignore ([4c6aade](https://github.com/LinkForty/mobile-sdk-expo/commit/4c6aade))
|
|
24
|
+
|
|
25
|
+
# Changelog
|
|
26
|
+
|
|
27
|
+
All notable changes to this project will be documented in this file.
|
|
28
|
+
|
|
29
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
30
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
31
|
+
|
|
32
|
+
## [1.1.0] - 2026-03-03
|
|
33
|
+
### Added
|
|
34
|
+
- `externalUserId` option in `CreateLinkOptions` — identifies the app user creating the link, enabling per-user deduplication and share attribution
|
|
35
|
+
- `deduplicated` boolean in `CreateLinkResult` — indicates when an existing link was returned instead of creating a new one (per-user deduplication)
|
|
36
|
+
|
|
37
|
+
## [1.0.3] - 2026-02-27
|
|
38
|
+
### Changed
|
|
39
|
+
- Optimized README and npm package description for LLM discoverability
|
|
40
|
+
- Added competitor positioning keywords and cross-SDK references
|
|
41
|
+
- Added `firebase-dynamic-links` npm keyword
|
|
42
|
+
|
|
43
|
+
## [1.0.2] - 2026-02-27
|
|
44
|
+
### Fixed
|
|
45
|
+
- Corrected GitHub repository, bugs, and homepage URLs in package.json
|
|
46
|
+
|
|
47
|
+
## [1.0.1] - 2026-02-26
|
|
48
|
+
### Added
|
|
49
|
+
- `llms.txt` — LLM-optimized integration reference shipped with the published package, enabling AI coding assistants to generate accurate integration code directly from node_modules
|
|
50
|
+
|
|
51
|
+
## [1.0.0] - 2026-02-16
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- Initial release of the LinkForty Expo SDK
|
|
55
|
+
- Initialization with config validation (HTTPS enforcement, attribution window bounds)
|
|
56
|
+
- Deferred deep linking (install attribution via device fingerprinting)
|
|
57
|
+
- Direct deep linking with multiple callback support
|
|
58
|
+
- Server-side URL resolution with fingerprint query parameters
|
|
59
|
+
- Event tracking with install ID correlation
|
|
60
|
+
- Revenue tracking (`trackRevenue`)
|
|
61
|
+
- Programmatic link creation (simplified + dashboard endpoints)
|
|
62
|
+
- Attribution data access (`getInstallId`, `getInstallData`, `isFirstLaunch`)
|
|
63
|
+
- Persistent offline event queue (AsyncStorage-backed, max 100 events, FIFO)
|
|
64
|
+
- Manual queue management (`flushEvents`, `clearEventQueue`, `queuedEventCount`)
|
|
65
|
+
- Data management (`clearData`, `reset`)
|
|
66
|
+
- Typed error handling (`LinkFortyError` with `LinkFortyErrorCode` enum)
|
|
67
|
+
- Network retry with exponential backoff (3 attempts, 1s/2s/4s, no retry on 4xx)
|
|
68
|
+
- Debug logging via `debug` config flag
|
|
69
|
+
- Pure Expo module dependencies (no native linking required)
|
|
70
|
+
- Comprehensive Vitest test suite (119 tests)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parth24072001
|
|
4
|
+
Copyright (c) 2026 LinkForty
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# deep-link-expo
|
|
2
|
+
|
|
3
|
+
Expo SDK for the [deep-link](https://github.com/Parth24072001/deep-link) self-hosted service. Add deferred deep linking, install attribution, and short-link routing to an Expo app. Pure Expo modules — no native linking required.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/deep-link-expo)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx expo install deep-link-expo expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or with npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install deep-link-expo
|
|
18
|
+
npx expo install expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Create an app in the dashboard, copy the four values from the overview page, then initialize once at startup:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import DeepLink from 'deep-link-expo';
|
|
27
|
+
|
|
28
|
+
const response = await DeepLink.initialize({
|
|
29
|
+
baseUrl: 'https://go.yourdomain.com',
|
|
30
|
+
linkPathPrefix: '/your-app-prefix',
|
|
31
|
+
apiKey: 'pk_live_xxxxxxxx', // publishable key only
|
|
32
|
+
appToken: 'your-app-id', // dashboard App ID
|
|
33
|
+
debug: __DEV__,
|
|
34
|
+
attributionWindowHours: 168,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (response.attributed) {
|
|
38
|
+
console.log('Install attributed', response.deepLinkData);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DeepLink.onDeferredDeepLink((data) => {
|
|
42
|
+
if (data) {
|
|
43
|
+
// First open after install
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
DeepLink.onDeepLink((url, data) => {
|
|
48
|
+
// App already installed, user tapped a link
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use the publishable key (`pk_…`) in the mobile app. Never put the secret key (`sk_…`) in the client.
|
|
53
|
+
|
|
54
|
+
## What the SDK sends
|
|
55
|
+
|
|
56
|
+
On install/resolve the SDK reports a device fingerprint used for deferred matching: IP (server-side), user agent, timezone, language, screen size, and platform. Tracked events include the event name plus any properties you pass. Nothing is sent to a third-party analytics vendor — only to the `baseUrl` you configure.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- Deferred deep linking (install attribution via device fingerprinting)
|
|
61
|
+
- Direct deep linking with server-side URL resolution
|
|
62
|
+
- Event tracking with an offline queue (persists across app restarts)
|
|
63
|
+
- Revenue tracking
|
|
64
|
+
- Programmatic link creation (publishable or secret key, depending on your server policy)
|
|
65
|
+
- Pure Expo modules (`expo-device`, `expo-application`, `expo-localization`, `expo-linking`)
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
await DeepLink.initialize(config);
|
|
71
|
+
DeepLink.isInitialized;
|
|
72
|
+
|
|
73
|
+
DeepLink.onDeferredDeepLink(callback);
|
|
74
|
+
DeepLink.onDeepLink(callback);
|
|
75
|
+
DeepLink.handleDeepLink(url);
|
|
76
|
+
|
|
77
|
+
await DeepLink.trackEvent(name, properties?);
|
|
78
|
+
await DeepLink.trackRevenue(amount, currency, properties?);
|
|
79
|
+
await DeepLink.flushEvents();
|
|
80
|
+
await DeepLink.clearEventQueue();
|
|
81
|
+
DeepLink.queuedEventCount;
|
|
82
|
+
|
|
83
|
+
await DeepLink.getInstallId();
|
|
84
|
+
await DeepLink.getInstallData();
|
|
85
|
+
await DeepLink.isFirstLaunch();
|
|
86
|
+
await DeepLink.clearData();
|
|
87
|
+
DeepLink.reset();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Automatic screen tracking
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { createNavigationContainerRef } from '@react-navigation/native';
|
|
94
|
+
|
|
95
|
+
export const navigationRef = createNavigationContainerRef();
|
|
96
|
+
|
|
97
|
+
await DeepLink.initialize({
|
|
98
|
+
baseUrl: 'https://go.yourdomain.com',
|
|
99
|
+
linkPathPrefix: '/your-app-prefix',
|
|
100
|
+
autoTrackNavigation: true,
|
|
101
|
+
navigationRef,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Route params are off by default. To capture specific non-PII params:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
autoTrackNavigation: { captureParams: ['productId', 'category'] }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Link creation
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const result = await DeepLink.createLink({
|
|
115
|
+
deepLinkParameters: { route: 'PRODUCT', id: '123' },
|
|
116
|
+
title: 'Check out this product',
|
|
117
|
+
});
|
|
118
|
+
console.log(result.url);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
| Field | Required | Default | Description |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| `baseUrl` | yes | — | Your deep-link server origin (`https://…`, HTTP only for localhost) |
|
|
126
|
+
| `linkPathPrefix` | recommended | — | Per-app path from the dashboard (e.g. `/mQuDYblrf6`) |
|
|
127
|
+
| `apiKey` | for link create | — | Publishable API key (`pk_…`) |
|
|
128
|
+
| `appToken` | recommended | — | Dashboard App ID |
|
|
129
|
+
| `debug` | no | `false` | Verbose logging |
|
|
130
|
+
| `attributionWindowHours` | no | `168` | 1–2160 hours |
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT — see [LICENSE](LICENSE).
|
|
135
|
+
|
|
136
|
+
## Support
|
|
137
|
+
|
|
138
|
+
Issues: [github.com/Parth24072001/deep-link](https://github.com/Parth24072001/deep-link/issues)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttributionContext — last-click attribution + session tracking (SIT-237).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the React Native SDK. Every deep-link open (deferred install OR direct
|
|
5
|
+
* re-engagement) pins an active attribution context to THAT link; the newest
|
|
6
|
+
* open supersedes the previous one. Every tracked event is stamped with the
|
|
7
|
+
* active context + a session id so the backend can credit the link under a
|
|
8
|
+
* last-click + window model. The active context is persisted so a reopen without
|
|
9
|
+
* a new click still attributes to the last link; the session is in-memory (a cold
|
|
10
|
+
* start is a new session).
|
|
11
|
+
*
|
|
12
|
+
* Uses AsyncStorage directly (not StorageManager) to stay self-contained.
|
|
13
|
+
*/
|
|
14
|
+
import type { ActiveAttribution, AttributionStamp } from '../models/attribution';
|
|
15
|
+
export declare class AttributionContext {
|
|
16
|
+
private active;
|
|
17
|
+
private sessionId;
|
|
18
|
+
private loaded;
|
|
19
|
+
constructor();
|
|
20
|
+
/** Restore the persisted active context. Idempotent; never throws. */
|
|
21
|
+
load(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Record a deep-link open. Newest open supersedes (last-click) and starts a new
|
|
24
|
+
* session. No-op when no `linkId` is known (organic open).
|
|
25
|
+
*/
|
|
26
|
+
recordDeepLinkOpen(linkId?: string | null, clickId?: string | null): Promise<void>;
|
|
27
|
+
/** Fields to merge into every event payload. */
|
|
28
|
+
getStamp(): AttributionStamp;
|
|
29
|
+
getSessionId(): string;
|
|
30
|
+
getActive(): ActiveAttribution | null;
|
|
31
|
+
/** Clear the persisted context and start a fresh session. */
|
|
32
|
+
clear(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=attribution-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution-context.d.ts","sourceRoot":"","sources":["../../src/attribution/attribution-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAejF,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAS;;IAOvB,sEAAsE;IAChE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B;;;OAGG;IACG,kBAAkB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxF,gDAAgD;IAChD,QAAQ,IAAI,gBAAgB;IAS5B,YAAY,IAAI,MAAM;IAItB,SAAS,IAAI,iBAAiB,GAAG,IAAI;IAIrC,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAS7B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttributionContext — last-click attribution + session tracking (SIT-237).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the React Native SDK. Every deep-link open (deferred install OR direct
|
|
5
|
+
* re-engagement) pins an active attribution context to THAT link; the newest
|
|
6
|
+
* open supersedes the previous one. Every tracked event is stamped with the
|
|
7
|
+
* active context + a session id so the backend can credit the link under a
|
|
8
|
+
* last-click + window model. The active context is persisted so a reopen without
|
|
9
|
+
* a new click still attributes to the last link; the session is in-memory (a cold
|
|
10
|
+
* start is a new session).
|
|
11
|
+
*
|
|
12
|
+
* Uses AsyncStorage directly (not StorageManager) to stay self-contained.
|
|
13
|
+
*/
|
|
14
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
15
|
+
import { STORAGE_KEYS } from '../storage/storage-keys';
|
|
16
|
+
import { logger } from '../logger';
|
|
17
|
+
/**
|
|
18
|
+
* RFC4122-v4-style id for session grouping. Not a security token — `Math.random`
|
|
19
|
+
* is sufficient and avoids a native crypto dependency.
|
|
20
|
+
*/
|
|
21
|
+
function generateSessionId() {
|
|
22
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
23
|
+
const r = (Math.random() * 16) | 0;
|
|
24
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
25
|
+
return v.toString(16);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export class AttributionContext {
|
|
29
|
+
active = null;
|
|
30
|
+
sessionId;
|
|
31
|
+
loaded = false;
|
|
32
|
+
constructor() {
|
|
33
|
+
// Construction == cold start == a new session.
|
|
34
|
+
this.sessionId = generateSessionId();
|
|
35
|
+
}
|
|
36
|
+
/** Restore the persisted active context. Idempotent; never throws. */
|
|
37
|
+
async load() {
|
|
38
|
+
if (this.loaded)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const raw = await AsyncStorage.getItem(STORAGE_KEYS.ATTRIBUTION);
|
|
42
|
+
if (raw) {
|
|
43
|
+
this.active = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
logger.warn('Failed to load attribution context:', e);
|
|
48
|
+
}
|
|
49
|
+
this.loaded = true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Record a deep-link open. Newest open supersedes (last-click) and starts a new
|
|
53
|
+
* session. No-op when no `linkId` is known (organic open).
|
|
54
|
+
*/
|
|
55
|
+
async recordDeepLinkOpen(linkId, clickId) {
|
|
56
|
+
if (!linkId)
|
|
57
|
+
return;
|
|
58
|
+
this.active = {
|
|
59
|
+
linkId,
|
|
60
|
+
clickId: clickId ?? undefined,
|
|
61
|
+
openedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
this.sessionId = generateSessionId();
|
|
64
|
+
try {
|
|
65
|
+
await AsyncStorage.setItem(STORAGE_KEYS.ATTRIBUTION, JSON.stringify(this.active));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
logger.warn('Failed to persist attribution context:', e);
|
|
69
|
+
}
|
|
70
|
+
logger.log('Attribution context set:', this.active, 'session:', this.sessionId);
|
|
71
|
+
}
|
|
72
|
+
/** Fields to merge into every event payload. */
|
|
73
|
+
getStamp() {
|
|
74
|
+
return {
|
|
75
|
+
attributedLinkId: this.active?.linkId,
|
|
76
|
+
attributedClickId: this.active?.clickId,
|
|
77
|
+
linkOpenedAt: this.active?.openedAt,
|
|
78
|
+
sessionId: this.sessionId,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
getSessionId() {
|
|
82
|
+
return this.sessionId;
|
|
83
|
+
}
|
|
84
|
+
getActive() {
|
|
85
|
+
return this.active;
|
|
86
|
+
}
|
|
87
|
+
/** Clear the persisted context and start a fresh session. */
|
|
88
|
+
async clear() {
|
|
89
|
+
this.active = null;
|
|
90
|
+
this.sessionId = generateSessionId();
|
|
91
|
+
try {
|
|
92
|
+
await AsyncStorage.removeItem(STORAGE_KEYS.ATTRIBUTION);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
logger.warn('Failed to clear attribution context:', e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FingerprintCollectorProtocol } from '../fingerprint/fingerprint-collector';
|
|
2
|
+
import type { NetworkManagerProtocol } from '../network/network-manager';
|
|
3
|
+
import type { StorageManagerProtocol } from '../storage/storage-manager';
|
|
4
|
+
import type { InstallAttributionResponse } from '../models/install-response';
|
|
5
|
+
import type { DeepLinkData } from '../models/deep-link-data';
|
|
6
|
+
export declare class AttributionManager {
|
|
7
|
+
private readonly network;
|
|
8
|
+
private readonly storage;
|
|
9
|
+
private readonly fingerprint;
|
|
10
|
+
constructor(network: NetworkManagerProtocol, storage: StorageManagerProtocol, fingerprint: FingerprintCollectorProtocol);
|
|
11
|
+
reportInstall(attributionWindowHours: number, deviceId?: string, appToken?: string): Promise<InstallAttributionResponse>;
|
|
12
|
+
getInstallId(): Promise<string | null>;
|
|
13
|
+
getInstallData(): Promise<DeepLinkData | null>;
|
|
14
|
+
isFirstLaunch(): Promise<boolean>;
|
|
15
|
+
clearData(): Promise<void>;
|
|
16
|
+
private buildCachedResponse;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=attribution-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution-manager.d.ts","sourceRoot":"","sources":["../../src/attribution/attribution-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACzF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7D,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;gBAGzD,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,4BAA4B;IAOrC,aAAa,CACjB,sBAAsB,EAAE,MAAM,EAC9B,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,0BAA0B,CAAC;IAqEhC,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAItC,cAAc,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAI9C,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;YAKlB,mBAAmB;CAYlC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { logger } from '../logger';
|
|
2
|
+
import { SDK_NAME, SDK_VERSION } from '../version';
|
|
3
|
+
export class AttributionManager {
|
|
4
|
+
network;
|
|
5
|
+
storage;
|
|
6
|
+
fingerprint;
|
|
7
|
+
constructor(network, storage, fingerprint) {
|
|
8
|
+
this.network = network;
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
this.fingerprint = fingerprint;
|
|
11
|
+
}
|
|
12
|
+
async reportInstall(attributionWindowHours, deviceId, appToken) {
|
|
13
|
+
const isFirst = await this.storage.isFirstLaunch();
|
|
14
|
+
if (!isFirst) {
|
|
15
|
+
return this.buildCachedResponse();
|
|
16
|
+
}
|
|
17
|
+
const fp = this.fingerprint.collect(attributionWindowHours, deviceId);
|
|
18
|
+
logger.log('Reporting install with fingerprint:', fp);
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await this.network.request('/api/sdk/v1/install', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
// appToken (when provided) lets Cloud scope organic installs
|
|
24
|
+
// to the right workspace. Omitted from the body when undefined.
|
|
25
|
+
// sdkName/sdkVersion (SIT-235) identify the SDK for health diagnostics.
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
...fp,
|
|
28
|
+
...(appToken ? { appToken } : {}),
|
|
29
|
+
sdkName: SDK_NAME,
|
|
30
|
+
sdkVersion: SDK_VERSION,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
logger.error('Failed to report install:', e);
|
|
36
|
+
// Treat as organic on failure
|
|
37
|
+
await this.storage.setHasLaunched();
|
|
38
|
+
return {
|
|
39
|
+
installId: '',
|
|
40
|
+
attributed: false,
|
|
41
|
+
confidenceScore: 0,
|
|
42
|
+
matchedFactors: [],
|
|
43
|
+
deepLinkData: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
logger.log('Install response:', response);
|
|
47
|
+
// Cache install ID
|
|
48
|
+
if (response.installId) {
|
|
49
|
+
await this.storage.saveInstallId(response.installId);
|
|
50
|
+
}
|
|
51
|
+
// Cache deep link data if attributed
|
|
52
|
+
if (response.attributed && response.deepLinkData) {
|
|
53
|
+
// Normalize deepLinkParameters -> customParameters
|
|
54
|
+
const deepLinkData = {
|
|
55
|
+
...response.deepLinkData,
|
|
56
|
+
customParameters: response.deepLinkData
|
|
57
|
+
.deepLinkParameters ?? response.deepLinkData.customParameters,
|
|
58
|
+
};
|
|
59
|
+
await this.storage.saveInstallData(deepLinkData);
|
|
60
|
+
logger.log('Install attributed with confidence:', response.confidenceScore);
|
|
61
|
+
// Return with normalized data
|
|
62
|
+
response = { ...response, deepLinkData };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
logger.log('Organic install (no attribution)');
|
|
66
|
+
}
|
|
67
|
+
await this.storage.setHasLaunched();
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
async getInstallId() {
|
|
71
|
+
return this.storage.getInstallId();
|
|
72
|
+
}
|
|
73
|
+
async getInstallData() {
|
|
74
|
+
return this.storage.getInstallData();
|
|
75
|
+
}
|
|
76
|
+
async isFirstLaunch() {
|
|
77
|
+
return this.storage.isFirstLaunch();
|
|
78
|
+
}
|
|
79
|
+
async clearData() {
|
|
80
|
+
await this.storage.clearAll();
|
|
81
|
+
logger.log('Attribution data cleared');
|
|
82
|
+
}
|
|
83
|
+
async buildCachedResponse() {
|
|
84
|
+
const installId = await this.storage.getInstallId();
|
|
85
|
+
const deepLinkData = await this.storage.getInstallData();
|
|
86
|
+
return {
|
|
87
|
+
installId: installId ?? '',
|
|
88
|
+
attributed: deepLinkData !== null,
|
|
89
|
+
confidenceScore: deepLinkData ? 100 : 0,
|
|
90
|
+
matchedFactors: [],
|
|
91
|
+
deepLinkData,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FingerprintCollectorProtocol } from '../fingerprint/fingerprint-collector';
|
|
2
|
+
import type { NetworkManagerProtocol } from '../network/network-manager';
|
|
3
|
+
import type { DeepLinkData } from '../models/deep-link-data';
|
|
4
|
+
export type DeferredDeepLinkCallback = (deepLinkData: DeepLinkData | null) => void;
|
|
5
|
+
export type DeepLinkCallback = (url: string, deepLinkData: DeepLinkData | null) => void;
|
|
6
|
+
export declare class DeepLinkHandler {
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private network;
|
|
9
|
+
private fingerprint;
|
|
10
|
+
private linkPathPrefix;
|
|
11
|
+
private deferredCallbacks;
|
|
12
|
+
private deepLinkCallbacks;
|
|
13
|
+
private deferredDelivered;
|
|
14
|
+
private cachedDeferredData;
|
|
15
|
+
private subscription;
|
|
16
|
+
constructor(baseUrl: string, network: NetworkManagerProtocol, fingerprint: FingerprintCollectorProtocol, linkPathPrefix?: string);
|
|
17
|
+
/**
|
|
18
|
+
* Start listening for deep links via expo-linking.
|
|
19
|
+
*/
|
|
20
|
+
startListening(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Manually pass a deep link URL.
|
|
23
|
+
*/
|
|
24
|
+
handleDeepLink(url: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Register a deferred deep link callback.
|
|
27
|
+
* If data was already delivered, calls back immediately.
|
|
28
|
+
*/
|
|
29
|
+
onDeferredDeepLink(callback: DeferredDeepLinkCallback): void;
|
|
30
|
+
/**
|
|
31
|
+
* Register a direct deep link callback. Multiple callbacks supported.
|
|
32
|
+
*/
|
|
33
|
+
onDeepLink(callback: DeepLinkCallback): void;
|
|
34
|
+
/**
|
|
35
|
+
* Deliver deferred deep link data (called by SDK after install attribution).
|
|
36
|
+
*/
|
|
37
|
+
deliverDeferredDeepLink(data: DeepLinkData | null): void;
|
|
38
|
+
clearCallbacks(): void;
|
|
39
|
+
cleanup(): void;
|
|
40
|
+
private handleUrl;
|
|
41
|
+
private resolveUrl;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=deep-link-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-link-handler.d.ts","sourceRoot":"","sources":["../../src/deeplink/deep-link-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACzF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7D,MAAM,MAAM,wBAAwB,GAAG,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;AACnF,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;AAExF,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,cAAc,CAAqB;IAE3C,OAAO,CAAC,iBAAiB,CAAkC;IAC3D,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,YAAY,CAAmC;gBAGrD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,4BAA4B,EACzC,cAAc,CAAC,EAAE,MAAM;IAQzB;;OAEG;IACH,cAAc,IAAI,IAAI;IAiBtB;;OAEG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIjC;;;OAGG;IACH,kBAAkB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAQ5D;;OAEG;IACH,UAAU,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAI5C;;OAEG;IACH,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI;IAUxD,cAAc,IAAI,IAAI;IAOtB,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,SAAS;YAuBH,UAAU;CAyCzB"}
|