-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathattribution.ts
More file actions
182 lines (166 loc) · 6.27 KB
/
Copy pathattribution.ts
File metadata and controls
182 lines (166 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { Linking, Platform } from 'react-native';
import LinkTrail, { type LinkTrailDeepLink } from 'linktrail-react-native';
import type { StoreAction } from './store';
import { ConsentManager, type ConsentState } from './consent';
/**
* Bridges the demo UI to the LinkTrail SDK: configures it, forwards real
* deferred + re-engagement links into the store, and can *simulate* the four
* deferred scenarios locally so you can see each one without a real
* click → install round-trip. Mirrors the native examples' AttributionCoordinator.
*/
/**
* Your workspace SDK key (`lt_live_…`) from the LinkTrail dashboard. Replace
* this placeholder with your own — until you do, the backend rejects it
* (surfaced via `onError` as a console warning). The deep-link simulator
* works without a key.
*/
const API_KEY = 'lt_live_USE_YOUR_API_KEY';
/** One of the demo's deferred-deep-link scenarios. */
export interface Scenario {
id: string;
title: string;
detail: string;
link: LinkTrailDeepLink;
}
/** The four scenarios, each expressed as the link your `onLink` handler would receive. */
export const scenarios: Scenario[] = [
{
id: 'home',
title: 'Home',
detail: 'User just lands on the storefront',
link: fabricate('/', 'brand-awareness'),
},
{
id: 'category',
title: 'Home · Running selected',
detail: 'Lands on home with a category pre-selected',
link: fabricate('/category/running', 'running-sale'),
},
{
id: 'product',
title: 'Product · Air Jordan 1',
detail: 'Lands directly on a product page',
link: fabricate('/products/aj1', 'aj1-launch'),
},
{
id: 'voucher',
title: 'Product · Air Jordan 1 + voucher',
detail: 'Product page with a voucher from the link meta',
link: fabricate('/products/aj1', 'vip-loyalty', {
voucher: 'SUMMER25',
discountPercent: '25',
}),
},
];
function fabricate(
path: string,
campaign: string,
customData?: Record<string, string>,
): LinkTrailDeepLink {
return {
deepLinkPath: path,
campaign,
customData,
path,
hasRoutableDestination: path !== '/',
};
}
/**
* Configures the SDK and wires links into the store. Returns a cleanup
* function. `dispatch` is the store's reducer dispatch.
*/
export function startAttribution(
dispatch: (action: StoreAction) => void,
onConsentLoaded: (state: ConsentState) => void,
): () => void {
const subscriptions: { remove(): void }[] = [];
(async () => {
try {
// requireConsent → gate attribution/tracking behind the user's decision
// (deny-by-default). Deep links still route without consent.
// linkDomains → the hosts whose links are ours. Re-engagement opens (app
// already installed) are only routed for these hosts; a host missing here
// opens the app but never navigates. (Deferred install links skip this
// check, which is why they work regardless.)
await LinkTrail.configure(API_KEY, {
linkDomains: ['link.kynxlabs.com', 'kick.linktrail.io'],
requireConsent: true,
// 'pasteButton' → iOS reads the deferred click token only when the user
// taps <LinkTrailPasteButton/> (no "Allow Paste" alert). Ignored on Android.
clickTokenSource: 'pasteButton',
// iOS: false so the install waits for the paste tap (firing token-less at
// launch would mark the install tracked and make the tap a no-op).
// Android: true — there's no paste button; it uses the Play Install
// Referrer, so the install must auto-track for deferred attribution to
// resolve. (Consent still gates what's *recorded*; links route regardless.)
autoTrackInstall: Platform.OS !== 'ios',
});
} catch (error) {
console.warn('LinkTrail configuration failed:', error);
return;
}
// Subscribe to onLink FIRST — before any further `await` — so a cold-start
// deep link (deferred first-launch, or a Universal Link that launched the
// app) delivered right after `configure` isn't missed while we read consent
// from storage. Routing works even without consent (only tracking is gated).
subscriptions.push(
LinkTrail.onLink((link, source) => {
dispatch({ type: 'route-link', link, source });
}),
);
// Surface backend failures — most importantly a rejected API key.
subscriptions.push(
LinkTrail.onError((error) => {
if (error.code === 'invalid_api_key') {
console.warn(
'⚠️ LinkTrail: API key rejected by the server — check your lt_live_… key.',
);
} else {
console.warn(`⚠️ LinkTrail error [${error.code}]: ${error.message}`);
}
}),
);
LinkTrail.registerForSKAdAttribution();
// The SDK has no consent getter — replay our persisted decision to it on
// every launch, then tell the UI so it can show the prompt if undecided.
const consent = await ConsentManager.load();
ConsentManager.syncToSDK(consent);
onConsentLoaded(consent);
})();
// Offline demo: kickflip:// URLs are not LinkTrail links (the SDK ignores
// them), so route them locally — manual testing works while installed, e.g.
// xcrun simctl openurl booted "kickflip://products/aj1?voucher=SUMMER25&discountPercent=25"
Linking.getInitialURL().then((url) => url && routeLocalScheme(url, dispatch));
const linkingSub = Linking.addEventListener('url', ({ url }) =>
routeLocalScheme(url, dispatch),
);
return () => {
subscriptions.forEach((s) => s.remove());
linkingSub.remove();
};
}
function routeLocalScheme(
url: string,
dispatch: (action: StoreAction) => void,
): void {
if (!url.startsWith('kickflip://')) return;
const [pathPart = '', queryPart] = url.slice('kickflip://'.length).split('?');
const path = '/' + pathPart.replace(/^\/+|\/+$/g, '');
const customData: Record<string, string> = {};
queryPart?.split('&').forEach((pair) => {
const [key, value] = pair.split('=');
if (key && value !== undefined) {
customData[decodeURIComponent(key)] = decodeURIComponent(value);
}
});
dispatch({
type: 'route-link',
link: {
deepLinkPath: path,
customData: Object.keys(customData).length ? customData : undefined,
path,
hasRoutableDestination: path !== '/',
},
source: 'reengagement',
});
}