@tolinku/react-native-sdk 0.2.0 → 0.4.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 +31 -2
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/tolinku/rnsdk/TolinkuInstallReferrerModule.java +107 -0
- package/android/src/main/java/com/tolinku/rnsdk/TolinkuInstallReferrerPackage.java +30 -0
- package/dist/index.d.mts +71 -3
- package/dist/index.d.ts +71 -3
- package/dist/index.js +129 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -142,12 +142,41 @@ if (link) {
|
|
|
142
142
|
console.log(link.deep_link_path); // e.g. "/merchant/xyz"
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Claim by device signal matching
|
|
145
|
+
// Claim by device signal matching. Timezone, language, screen size, pixel ratio and
|
|
146
|
+
// OS version are detected for you.
|
|
146
147
|
const link = await Tolinku.deferred.claimBySignals({
|
|
147
|
-
appspaceId: '
|
|
148
|
+
appspaceId: '64f0a1b2c3d4e5f60718',
|
|
148
149
|
});
|
|
149
150
|
```
|
|
150
151
|
|
|
152
|
+
On Android the Play Install Referrer is the deterministic mechanism: a Tolinku
|
|
153
|
+
link attaches a token to the store URL, Play keeps it through the install, and
|
|
154
|
+
this SDK reads it back on first launch. It names the exact click, survives for
|
|
155
|
+
days, and does not depend on the network the device was on. Device signals are
|
|
156
|
+
the fallback, and the only option on iOS, where no equivalent exists.
|
|
157
|
+
|
|
158
|
+
Prefer `claimDeferredLink()` over choosing a mechanism yourself:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Referrer first, device signals as the fallback.
|
|
162
|
+
const link = await Tolinku.deferred.claimDeferredLink({
|
|
163
|
+
appspaceId: '64f0a1b2c3d4e5f60718',
|
|
164
|
+
});
|
|
165
|
+
if (link) routeTo(link.deep_link_path);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Call it once on first launch. Calling again is safe, but a claim is consumed the
|
|
169
|
+
first time it succeeds, so a second call returns nothing.
|
|
170
|
+
|
|
171
|
+
The Android side is bundled with this package, so there is nothing else to
|
|
172
|
+
install. Autolinking is per-platform, so an iOS-only app never builds it. In
|
|
173
|
+
Expo Go the native module is absent and Android falls back to signal matching;
|
|
174
|
+
a development build gets the referrer.
|
|
175
|
+
|
|
176
|
+
`appspaceId` is your Appspace ID, not your subdomain or slug. Copy it from the dashboard
|
|
177
|
+
under **Integrate** or **Settings**. It looks like `64f0a1b2c3d4e5f60718`.
|
|
178
|
+
|
|
179
|
+
|
|
151
180
|
### In-App Messages (React Native Component)
|
|
152
181
|
|
|
153
182
|
Display server-configured in-app messages using the `<TolinkuMessages>` component. Drop it anywhere in your React Native component tree to automatically fetch and render messages as a modal overlay. Messages are created and managed from the Tolinku dashboard without shipping app updates.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Android side of @tolinku/react-native-sdk.
|
|
2
|
+
//
|
|
3
|
+
// Exists only to read the Play Install Referrer, which is the deterministic way
|
|
4
|
+
// a deferred link survives an install on Android. It is bundled rather than
|
|
5
|
+
// delegated to a third-party package because the referrer token is Tolinku's
|
|
6
|
+
// own mechanism: we mint it on the link and read it back here.
|
|
7
|
+
//
|
|
8
|
+
// Autolinking is per-platform, so this is not built when compiling for iOS. An
|
|
9
|
+
// iOS-only app pays nothing for its presence.
|
|
10
|
+
//
|
|
11
|
+
// To verify compilation outside a host app:
|
|
12
|
+
// printf 'rootProject.name = "t"\n' > android/settings.gradle
|
|
13
|
+
// ../../android-sdk/gradlew -p android assembleRelease \
|
|
14
|
+
// -PtolinkuReactAndroidVersion=0.73.11 -Pandroid.useAndroidX=true
|
|
15
|
+
// rm android/settings.gradle
|
|
16
|
+
|
|
17
|
+
buildscript {
|
|
18
|
+
repositories {
|
|
19
|
+
google()
|
|
20
|
+
mavenCentral()
|
|
21
|
+
}
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath("com.android.tools.build:gradle:8.1.4")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
apply plugin: "com.android.library"
|
|
28
|
+
|
|
29
|
+
def safeExtGet(prop, fallback) {
|
|
30
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
android {
|
|
34
|
+
// The peer range starts at React Native 0.72, whose AGP 7.4 supports
|
|
35
|
+
// namespace, so the old manifest package attribute is not needed.
|
|
36
|
+
namespace "com.tolinku.rnsdk"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
// The Install Referrer library needs 21; the host app decides above that.
|
|
42
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
43
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
compileOptions {
|
|
47
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
48
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lintOptions {
|
|
52
|
+
abortOnError false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
repositories {
|
|
57
|
+
google()
|
|
58
|
+
mavenCentral()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependencies {
|
|
62
|
+
// Unversioned on purpose: the React Native Gradle plugin in the host app
|
|
63
|
+
// pins this to whatever version the app is on, so the library follows the
|
|
64
|
+
// app rather than forcing one. A version is supplied only for the standalone
|
|
65
|
+
// build used to verify compilation outside an app.
|
|
66
|
+
def rnVersion = project.findProperty("tolinkuReactAndroidVersion")
|
|
67
|
+
if (rnVersion) {
|
|
68
|
+
implementation "com.facebook.react:react-android:${rnVersion}"
|
|
69
|
+
} else {
|
|
70
|
+
implementation "com.facebook.react:react-android"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
implementation "com.android.installreferrer:installreferrer:2.2"
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.tolinku.rnsdk;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.android.installreferrer.api.InstallReferrerClient;
|
|
6
|
+
import com.android.installreferrer.api.InstallReferrerStateListener;
|
|
7
|
+
import com.facebook.react.bridge.Promise;
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
9
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
11
|
+
|
|
12
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads the Play Install Referrer.
|
|
16
|
+
*
|
|
17
|
+
* A Tolinku link sends an Android visitor to the store with
|
|
18
|
+
* {@code referrer=tolk_token=<token>} attached. Play keeps that string through
|
|
19
|
+
* the install and hands it back on first launch, which names the exact click
|
|
20
|
+
* instead of inferring it from device signals.
|
|
21
|
+
*
|
|
22
|
+
* Only the raw referrer string is returned. Finding our token inside it is done
|
|
23
|
+
* in JavaScript, where it is covered by tests that run on every platform rather
|
|
24
|
+
* than only where an Android toolchain exists.
|
|
25
|
+
*/
|
|
26
|
+
public class TolinkuInstallReferrerModule extends ReactContextBaseJavaModule {
|
|
27
|
+
|
|
28
|
+
public static final String NAME = "TolinkuInstallReferrer";
|
|
29
|
+
|
|
30
|
+
public TolinkuInstallReferrerModule(ReactApplicationContext reactContext) {
|
|
31
|
+
super(reactContext);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Override
|
|
35
|
+
@NonNull
|
|
36
|
+
public String getName() {
|
|
37
|
+
return NAME;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves with the raw referrer string, or null when there is nothing to
|
|
42
|
+
* report.
|
|
43
|
+
*
|
|
44
|
+
* Never rejects. An organic install, a device without Play Services, and a
|
|
45
|
+
* store other than Play are all ordinary outcomes rather than errors, and
|
|
46
|
+
* the caller falls back to signal matching for every one of them. Rejecting
|
|
47
|
+
* would turn a routine absence into an unhandled promise on first launch.
|
|
48
|
+
*/
|
|
49
|
+
@ReactMethod
|
|
50
|
+
public void getInstallReferrer(final Promise promise) {
|
|
51
|
+
final InstallReferrerClient client;
|
|
52
|
+
try {
|
|
53
|
+
client = InstallReferrerClient.newBuilder(getReactApplicationContext()).build();
|
|
54
|
+
} catch (Throwable t) {
|
|
55
|
+
promise.resolve(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The Play listener can fire more than once on some devices, and
|
|
60
|
+
// resolving a promise twice is an error in React Native.
|
|
61
|
+
final AtomicBoolean settled = new AtomicBoolean(false);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
client.startConnection(new InstallReferrerStateListener() {
|
|
65
|
+
@Override
|
|
66
|
+
public void onInstallReferrerSetupFinished(int responseCode) {
|
|
67
|
+
if (!settled.compareAndSet(false, true)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
String referrer = null;
|
|
71
|
+
try {
|
|
72
|
+
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
73
|
+
referrer = client.getInstallReferrer().getInstallReferrer();
|
|
74
|
+
}
|
|
75
|
+
} catch (Throwable t) {
|
|
76
|
+
referrer = null;
|
|
77
|
+
} finally {
|
|
78
|
+
endQuietly(client);
|
|
79
|
+
}
|
|
80
|
+
promise.resolve(referrer);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Override
|
|
84
|
+
public void onInstallReferrerServiceDisconnected() {
|
|
85
|
+
if (!settled.compareAndSet(false, true)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
endQuietly(client);
|
|
89
|
+
promise.resolve(null);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
} catch (Throwable t) {
|
|
93
|
+
if (settled.compareAndSet(false, true)) {
|
|
94
|
+
endQuietly(client);
|
|
95
|
+
promise.resolve(null);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static void endQuietly(InstallReferrerClient client) {
|
|
101
|
+
try {
|
|
102
|
+
client.endConnection();
|
|
103
|
+
} catch (Throwable ignored) {
|
|
104
|
+
// Already gone.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.tolinku.rnsdk;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
/** Registers the install referrer module with React Native's autolinking. */
|
|
15
|
+
public class TolinkuInstallReferrerPackage implements ReactPackage {
|
|
16
|
+
|
|
17
|
+
@Override
|
|
18
|
+
@NonNull
|
|
19
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
20
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
21
|
+
modules.add(new TolinkuInstallReferrerModule(reactContext));
|
|
22
|
+
return modules;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
@NonNull
|
|
27
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
28
|
+
return Collections.emptyList();
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -111,6 +111,10 @@ interface ClaimBySignalsOptions {
|
|
|
111
111
|
language?: string;
|
|
112
112
|
screenWidth?: number;
|
|
113
113
|
screenHeight?: number;
|
|
114
|
+
/** Defaults to `PixelRatio.get()`. Override only if you report a custom screen size. */
|
|
115
|
+
devicePixelRatio?: number;
|
|
116
|
+
/** Defaults to `Platform.Version`. Compared on the major component only. */
|
|
117
|
+
osVersion?: string;
|
|
114
118
|
}
|
|
115
119
|
/** In-app message from the API */
|
|
116
120
|
interface Message {
|
|
@@ -315,13 +319,68 @@ declare class Referrals {
|
|
|
315
319
|
}>;
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Pull our token out of a Play referrer string.
|
|
324
|
+
*
|
|
325
|
+
* The referrer is shared. A developer's own `utm_source` and anything else they
|
|
326
|
+
* attached sit in the same string, so the token is found among the pairs rather
|
|
327
|
+
* than assumed to be the whole value. A percent-encoded `%3D` is tolerated
|
|
328
|
+
* because Play normally decodes it, and that assumption is not worth a lost
|
|
329
|
+
* install if it is ever wrong.
|
|
330
|
+
*/
|
|
331
|
+
declare function parseInstallReferrer(referrer: string | null | undefined): string | null;
|
|
332
|
+
/** Anything that can hand back a Play referrer string. */
|
|
333
|
+
type ReferrerProvider = () => Promise<string | null> | string | null;
|
|
334
|
+
/**
|
|
335
|
+
* Best effort at the referrer token for this install.
|
|
336
|
+
*
|
|
337
|
+
* Android only, and null everywhere else: there is no equivalent on iOS, which
|
|
338
|
+
* is why signal matching exists at all. Never throws, because attribution must
|
|
339
|
+
* not be able to fail a first launch.
|
|
340
|
+
*/
|
|
341
|
+
declare function getInstallReferrerToken(provider?: ReferrerProvider): Promise<string | null>;
|
|
342
|
+
|
|
318
343
|
declare class Deferred {
|
|
319
344
|
private client;
|
|
320
345
|
constructor(client: HttpClient);
|
|
321
346
|
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
322
|
-
claimByToken(token: string): Promise<DeferredLink | null>;
|
|
347
|
+
claimByToken(token: string, appspaceId?: string): Promise<DeferredLink | null>;
|
|
348
|
+
/**
|
|
349
|
+
* Recover the link that led to this install, trying both mechanisms.
|
|
350
|
+
*
|
|
351
|
+
* The Play Install Referrer is asked first on Android: it names the exact
|
|
352
|
+
* click, survives for days, and does not care which network the device was
|
|
353
|
+
* on. Device signals are the fallback, and the only option on iOS, where no
|
|
354
|
+
* equivalent exists.
|
|
355
|
+
*
|
|
356
|
+
* Call once on first launch. Safe to call again, but a claim is consumed the
|
|
357
|
+
* first time it succeeds, so a second call returns null.
|
|
358
|
+
*
|
|
359
|
+
* Reading the referrer needs a native Play Services binding, which this
|
|
360
|
+
* package deliberately does not bundle. Pass `referrerProvider`, or install a
|
|
361
|
+
* supported referrer package and it is used automatically. Without either,
|
|
362
|
+
* Android falls back to signal matching.
|
|
363
|
+
*/
|
|
364
|
+
claimDeferredLink(options: {
|
|
365
|
+
appspaceId: string;
|
|
366
|
+
referrerProvider?: ReferrerProvider;
|
|
367
|
+
/** Claim again even if an attempt was already recorded. For tests. */
|
|
368
|
+
force?: boolean;
|
|
369
|
+
}): Promise<DeferredLink | null>;
|
|
370
|
+
private alreadyAttempted;
|
|
371
|
+
private rememberAttempt;
|
|
323
372
|
/** Claim a deferred deep link by device signal matching */
|
|
324
373
|
claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
|
|
374
|
+
/**
|
|
375
|
+
* The signal claim, with whether the server actually answered.
|
|
376
|
+
*
|
|
377
|
+
* `settled` separates "nothing is waiting for this device", which no amount
|
|
378
|
+
* of asking will change, from "the request never got there". Both surface as
|
|
379
|
+
* null to callers of claimBySignals, but claimDeferredLink has to tell them
|
|
380
|
+
* apart: recording an attempt that never reached the server would spend an
|
|
381
|
+
* install's one chance at attribution on a dropped connection.
|
|
382
|
+
*/
|
|
383
|
+
private attemptSignals;
|
|
325
384
|
}
|
|
326
385
|
|
|
327
386
|
/**
|
|
@@ -334,7 +393,7 @@ declare class Deferred {
|
|
|
334
393
|
* await Tolinku.track('signup', { source: 'onboarding' });
|
|
335
394
|
*/
|
|
336
395
|
declare class Tolinku {
|
|
337
|
-
static readonly VERSION = "0.
|
|
396
|
+
static readonly VERSION = "0.4.0";
|
|
338
397
|
private static client;
|
|
339
398
|
private static analyticsInstance;
|
|
340
399
|
private static ecommerceInstance;
|
|
@@ -348,6 +407,15 @@ declare class Tolinku {
|
|
|
348
407
|
* If init() is called a second time without calling destroy() first,
|
|
349
408
|
* a warning is logged and the existing instance is returned.
|
|
350
409
|
*/
|
|
410
|
+
/**
|
|
411
|
+
* Configure the SDK.
|
|
412
|
+
*
|
|
413
|
+
* The name the Android, iOS and Flutter SDKs use for this. {@link init} does
|
|
414
|
+
* the same thing and still works; it is what this package shipped and
|
|
415
|
+
* breaking it would serve nobody. It is meant for deprecation later, once
|
|
416
|
+
* moving off it is a one-line change rather than a surprise.
|
|
417
|
+
*/
|
|
418
|
+
static configure(config: TolinkuConfig): void;
|
|
351
419
|
static init(config: TolinkuConfig): void;
|
|
352
420
|
/** Check whether the SDK has been initialized. */
|
|
353
421
|
static isConfigured(): boolean;
|
|
@@ -413,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
|
|
|
413
481
|
*/
|
|
414
482
|
declare function isSafeUrl(url: string): boolean;
|
|
415
483
|
|
|
416
|
-
export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, isSafeUrl };
|
|
484
|
+
export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type ReferrerProvider, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
|
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,10 @@ interface ClaimBySignalsOptions {
|
|
|
111
111
|
language?: string;
|
|
112
112
|
screenWidth?: number;
|
|
113
113
|
screenHeight?: number;
|
|
114
|
+
/** Defaults to `PixelRatio.get()`. Override only if you report a custom screen size. */
|
|
115
|
+
devicePixelRatio?: number;
|
|
116
|
+
/** Defaults to `Platform.Version`. Compared on the major component only. */
|
|
117
|
+
osVersion?: string;
|
|
114
118
|
}
|
|
115
119
|
/** In-app message from the API */
|
|
116
120
|
interface Message {
|
|
@@ -315,13 +319,68 @@ declare class Referrals {
|
|
|
315
319
|
}>;
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Pull our token out of a Play referrer string.
|
|
324
|
+
*
|
|
325
|
+
* The referrer is shared. A developer's own `utm_source` and anything else they
|
|
326
|
+
* attached sit in the same string, so the token is found among the pairs rather
|
|
327
|
+
* than assumed to be the whole value. A percent-encoded `%3D` is tolerated
|
|
328
|
+
* because Play normally decodes it, and that assumption is not worth a lost
|
|
329
|
+
* install if it is ever wrong.
|
|
330
|
+
*/
|
|
331
|
+
declare function parseInstallReferrer(referrer: string | null | undefined): string | null;
|
|
332
|
+
/** Anything that can hand back a Play referrer string. */
|
|
333
|
+
type ReferrerProvider = () => Promise<string | null> | string | null;
|
|
334
|
+
/**
|
|
335
|
+
* Best effort at the referrer token for this install.
|
|
336
|
+
*
|
|
337
|
+
* Android only, and null everywhere else: there is no equivalent on iOS, which
|
|
338
|
+
* is why signal matching exists at all. Never throws, because attribution must
|
|
339
|
+
* not be able to fail a first launch.
|
|
340
|
+
*/
|
|
341
|
+
declare function getInstallReferrerToken(provider?: ReferrerProvider): Promise<string | null>;
|
|
342
|
+
|
|
318
343
|
declare class Deferred {
|
|
319
344
|
private client;
|
|
320
345
|
constructor(client: HttpClient);
|
|
321
346
|
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
322
|
-
claimByToken(token: string): Promise<DeferredLink | null>;
|
|
347
|
+
claimByToken(token: string, appspaceId?: string): Promise<DeferredLink | null>;
|
|
348
|
+
/**
|
|
349
|
+
* Recover the link that led to this install, trying both mechanisms.
|
|
350
|
+
*
|
|
351
|
+
* The Play Install Referrer is asked first on Android: it names the exact
|
|
352
|
+
* click, survives for days, and does not care which network the device was
|
|
353
|
+
* on. Device signals are the fallback, and the only option on iOS, where no
|
|
354
|
+
* equivalent exists.
|
|
355
|
+
*
|
|
356
|
+
* Call once on first launch. Safe to call again, but a claim is consumed the
|
|
357
|
+
* first time it succeeds, so a second call returns null.
|
|
358
|
+
*
|
|
359
|
+
* Reading the referrer needs a native Play Services binding, which this
|
|
360
|
+
* package deliberately does not bundle. Pass `referrerProvider`, or install a
|
|
361
|
+
* supported referrer package and it is used automatically. Without either,
|
|
362
|
+
* Android falls back to signal matching.
|
|
363
|
+
*/
|
|
364
|
+
claimDeferredLink(options: {
|
|
365
|
+
appspaceId: string;
|
|
366
|
+
referrerProvider?: ReferrerProvider;
|
|
367
|
+
/** Claim again even if an attempt was already recorded. For tests. */
|
|
368
|
+
force?: boolean;
|
|
369
|
+
}): Promise<DeferredLink | null>;
|
|
370
|
+
private alreadyAttempted;
|
|
371
|
+
private rememberAttempt;
|
|
323
372
|
/** Claim a deferred deep link by device signal matching */
|
|
324
373
|
claimBySignals(options: ClaimBySignalsOptions): Promise<DeferredLink | null>;
|
|
374
|
+
/**
|
|
375
|
+
* The signal claim, with whether the server actually answered.
|
|
376
|
+
*
|
|
377
|
+
* `settled` separates "nothing is waiting for this device", which no amount
|
|
378
|
+
* of asking will change, from "the request never got there". Both surface as
|
|
379
|
+
* null to callers of claimBySignals, but claimDeferredLink has to tell them
|
|
380
|
+
* apart: recording an attempt that never reached the server would spend an
|
|
381
|
+
* install's one chance at attribution on a dropped connection.
|
|
382
|
+
*/
|
|
383
|
+
private attemptSignals;
|
|
325
384
|
}
|
|
326
385
|
|
|
327
386
|
/**
|
|
@@ -334,7 +393,7 @@ declare class Deferred {
|
|
|
334
393
|
* await Tolinku.track('signup', { source: 'onboarding' });
|
|
335
394
|
*/
|
|
336
395
|
declare class Tolinku {
|
|
337
|
-
static readonly VERSION = "0.
|
|
396
|
+
static readonly VERSION = "0.4.0";
|
|
338
397
|
private static client;
|
|
339
398
|
private static analyticsInstance;
|
|
340
399
|
private static ecommerceInstance;
|
|
@@ -348,6 +407,15 @@ declare class Tolinku {
|
|
|
348
407
|
* If init() is called a second time without calling destroy() first,
|
|
349
408
|
* a warning is logged and the existing instance is returned.
|
|
350
409
|
*/
|
|
410
|
+
/**
|
|
411
|
+
* Configure the SDK.
|
|
412
|
+
*
|
|
413
|
+
* The name the Android, iOS and Flutter SDKs use for this. {@link init} does
|
|
414
|
+
* the same thing and still works; it is what this package shipped and
|
|
415
|
+
* breaking it would serve nobody. It is meant for deprecation later, once
|
|
416
|
+
* moving off it is a one-line change rather than a surprise.
|
|
417
|
+
*/
|
|
418
|
+
static configure(config: TolinkuConfig): void;
|
|
351
419
|
static init(config: TolinkuConfig): void;
|
|
352
420
|
/** Check whether the SDK has been initialized. */
|
|
353
421
|
static isConfigured(): boolean;
|
|
@@ -413,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
|
|
|
413
481
|
*/
|
|
414
482
|
declare function isSafeUrl(url: string): boolean;
|
|
415
483
|
|
|
416
|
-
export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, isSafeUrl };
|
|
484
|
+
export { type AddPaymentInfoParams, type AddToCartParams, type AddToWishlistParams, type BeginCheckoutParams, type ClaimBySignalsOptions, type CompleteReferralOptions, type CompleteReferralResult, type CreateReferralOptions, type CreateReferralResult, type DeferredLink, type EcommerceItem, type LeaderboardEntry, type Message, type MessageComponent, type MessageContent, type MilestoneOptions, type MilestoneResult, type PurchaseParams, type RateParams, type ReferralInfo, type ReferrerProvider, type RefundParams, type RemoveFromCartParams, type ResolvedTolinkuConfig, type SearchParams, type ShareParams, type ShowMessageOptions, type SpendCreditsParams, Tolinku, type TolinkuConfig, TolinkuError, TolinkuMessages, type TrackProperties, type ViewItemParams, getInstallReferrerToken, isSafeUrl, parseInstallReferrer };
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
10
10
|
var AsyncStorage__default = /*#__PURE__*/_interopDefault(AsyncStorage);
|
|
11
11
|
|
|
12
12
|
// src/types.ts
|
|
13
|
-
var SDK_VERSION = "0.
|
|
13
|
+
var SDK_VERSION = "0.4.0";
|
|
14
14
|
|
|
15
15
|
// src/debug.ts
|
|
16
16
|
var _debugEnabled = false;
|
|
@@ -630,41 +630,150 @@ var Referrals = class {
|
|
|
630
630
|
});
|
|
631
631
|
}
|
|
632
632
|
};
|
|
633
|
+
var TOKEN_KEY = "tolk_token";
|
|
634
|
+
function parseInstallReferrer(referrer) {
|
|
635
|
+
if (!referrer || !referrer.trim()) return null;
|
|
636
|
+
let decoded = referrer;
|
|
637
|
+
try {
|
|
638
|
+
decoded = decodeURIComponent(referrer);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
const pair = decoded.split("&").map((p) => p.trim()).find((p) => p.startsWith(`${TOKEN_KEY}=`));
|
|
642
|
+
if (!pair) return null;
|
|
643
|
+
const token = pair.slice(TOKEN_KEY.length + 1);
|
|
644
|
+
return token.trim() ? token : null;
|
|
645
|
+
}
|
|
646
|
+
function nativeProvider() {
|
|
647
|
+
const native = reactNative.NativeModules?.TolinkuInstallReferrer;
|
|
648
|
+
if (!native || typeof native.getInstallReferrer !== "function") return null;
|
|
649
|
+
return () => native.getInstallReferrer();
|
|
650
|
+
}
|
|
651
|
+
async function getInstallReferrerToken(provider) {
|
|
652
|
+
if (reactNative.Platform.OS !== "android") return null;
|
|
653
|
+
const source = provider ?? nativeProvider();
|
|
654
|
+
if (!source) return null;
|
|
655
|
+
try {
|
|
656
|
+
const referrer = await source();
|
|
657
|
+
return parseInstallReferrer(referrer);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
debugWarn(`Install referrer lookup failed: ${err.message}`);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/deferred.ts
|
|
665
|
+
var CLAIMED_KEY = "tolinku_deferred_claimed";
|
|
633
666
|
var Deferred = class {
|
|
634
667
|
constructor(client) {
|
|
635
668
|
this.client = client;
|
|
636
669
|
}
|
|
637
670
|
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
638
|
-
async claimByToken(token) {
|
|
671
|
+
async claimByToken(token, appspaceId) {
|
|
639
672
|
if (!token || !token.trim()) {
|
|
640
673
|
throw new Error("Tolinku: token is required and must not be blank for claimByToken.");
|
|
641
674
|
}
|
|
642
675
|
try {
|
|
643
|
-
return await this.client.getPublic("/v1/api/deferred/claim", {
|
|
676
|
+
return await this.client.getPublic("/v1/api/deferred/claim", {
|
|
677
|
+
token,
|
|
678
|
+
...appspaceId ? { appspace_id: appspaceId } : {}
|
|
679
|
+
});
|
|
644
680
|
} catch (err) {
|
|
645
681
|
debugWarn(`Deferred claimByToken failed: ${err.message}`);
|
|
646
682
|
return null;
|
|
647
683
|
}
|
|
648
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Recover the link that led to this install, trying both mechanisms.
|
|
687
|
+
*
|
|
688
|
+
* The Play Install Referrer is asked first on Android: it names the exact
|
|
689
|
+
* click, survives for days, and does not care which network the device was
|
|
690
|
+
* on. Device signals are the fallback, and the only option on iOS, where no
|
|
691
|
+
* equivalent exists.
|
|
692
|
+
*
|
|
693
|
+
* Call once on first launch. Safe to call again, but a claim is consumed the
|
|
694
|
+
* first time it succeeds, so a second call returns null.
|
|
695
|
+
*
|
|
696
|
+
* Reading the referrer needs a native Play Services binding, which this
|
|
697
|
+
* package deliberately does not bundle. Pass `referrerProvider`, or install a
|
|
698
|
+
* supported referrer package and it is used automatically. Without either,
|
|
699
|
+
* Android falls back to signal matching.
|
|
700
|
+
*/
|
|
701
|
+
async claimDeferredLink(options) {
|
|
702
|
+
if (!options.appspaceId || !options.appspaceId.trim()) {
|
|
703
|
+
throw new Error("Tolinku: appspaceId is required and must not be blank for claimDeferredLink.");
|
|
704
|
+
}
|
|
705
|
+
if (!options.force && await this.alreadyAttempted()) return null;
|
|
706
|
+
const token = await getInstallReferrerToken(options.referrerProvider);
|
|
707
|
+
if (token) {
|
|
708
|
+
const byToken = await this.claimByToken(token, options.appspaceId).catch(() => null);
|
|
709
|
+
if (byToken) {
|
|
710
|
+
await this.rememberAttempt();
|
|
711
|
+
return byToken;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const { link, settled } = await this.attemptSignals({ appspaceId: options.appspaceId });
|
|
715
|
+
if (settled) await this.rememberAttempt();
|
|
716
|
+
return link;
|
|
717
|
+
}
|
|
718
|
+
async alreadyAttempted() {
|
|
719
|
+
try {
|
|
720
|
+
return await AsyncStorage__default.default.getItem(CLAIMED_KEY) !== null;
|
|
721
|
+
} catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async rememberAttempt() {
|
|
726
|
+
try {
|
|
727
|
+
await AsyncStorage__default.default.setItem(CLAIMED_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
649
731
|
/** Claim a deferred deep link by device signal matching */
|
|
650
732
|
async claimBySignals(options) {
|
|
651
733
|
if (!options.appspaceId || !options.appspaceId.trim()) {
|
|
652
734
|
throw new Error("Tolinku: appspaceId is required and must not be blank for claimBySignals.");
|
|
653
735
|
}
|
|
736
|
+
return (await this.attemptSignals(options)).link;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The signal claim, with whether the server actually answered.
|
|
740
|
+
*
|
|
741
|
+
* `settled` separates "nothing is waiting for this device", which no amount
|
|
742
|
+
* of asking will change, from "the request never got there". Both surface as
|
|
743
|
+
* null to callers of claimBySignals, but claimDeferredLink has to tell them
|
|
744
|
+
* apart: recording an attempt that never reached the server would spend an
|
|
745
|
+
* install's one chance at attribution on a dropped connection.
|
|
746
|
+
*/
|
|
747
|
+
async attemptSignals(options) {
|
|
654
748
|
try {
|
|
655
749
|
const { width, height } = reactNative.Dimensions.get("screen");
|
|
656
750
|
const resolvedTimezone = options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
657
|
-
const resolvedLanguage = options.language || "en";
|
|
658
|
-
|
|
751
|
+
const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
|
|
752
|
+
const link = await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
|
|
659
753
|
appspace_id: options.appspaceId,
|
|
660
754
|
timezone: resolvedTimezone,
|
|
661
755
|
language: resolvedLanguage,
|
|
662
756
|
screen_width: options.screenWidth || width,
|
|
663
|
-
screen_height: options.screenHeight || height
|
|
757
|
+
screen_height: options.screenHeight || height,
|
|
758
|
+
// Separates devices reporting identical dp dimensions.
|
|
759
|
+
device_pixel_ratio: options.devicePixelRatio || reactNative.PixelRatio.get(),
|
|
760
|
+
os_version: options.osVersion || String(reactNative.Platform.Version)
|
|
664
761
|
});
|
|
762
|
+
return { link, settled: true };
|
|
665
763
|
} catch (err) {
|
|
764
|
+
const status = err?.statusCode ?? err?.status;
|
|
765
|
+
if (status === 404) {
|
|
766
|
+
debugWarn("Deferred claimBySignals: no match for this device.");
|
|
767
|
+
return { link: null, settled: true };
|
|
768
|
+
}
|
|
769
|
+
if (status === 403) {
|
|
770
|
+
console.warn(
|
|
771
|
+
`[Tolinku] claimBySignals failed with HTTP 403. Check that appspaceId is your Appspace ID (copy it from the dashboard under Settings), not your subdomain or slug. ${err.message}`
|
|
772
|
+
);
|
|
773
|
+
return { link: null, settled: false };
|
|
774
|
+
}
|
|
666
775
|
debugWarn(`Deferred claimBySignals failed: ${err.message}`);
|
|
667
|
-
return null;
|
|
776
|
+
return { link: null, settled: false };
|
|
668
777
|
}
|
|
669
778
|
}
|
|
670
779
|
};
|
|
@@ -754,6 +863,17 @@ var _Tolinku = class _Tolinku {
|
|
|
754
863
|
* If init() is called a second time without calling destroy() first,
|
|
755
864
|
* a warning is logged and the existing instance is returned.
|
|
756
865
|
*/
|
|
866
|
+
/**
|
|
867
|
+
* Configure the SDK.
|
|
868
|
+
*
|
|
869
|
+
* The name the Android, iOS and Flutter SDKs use for this. {@link init} does
|
|
870
|
+
* the same thing and still works; it is what this package shipped and
|
|
871
|
+
* breaking it would serve nobody. It is meant for deprecation later, once
|
|
872
|
+
* moving off it is a one-line change rather than a surprise.
|
|
873
|
+
*/
|
|
874
|
+
static configure(config) {
|
|
875
|
+
_Tolinku.init(config);
|
|
876
|
+
}
|
|
757
877
|
static init(config) {
|
|
758
878
|
if (!config.apiKey) throw new Error("Tolinku: apiKey is required");
|
|
759
879
|
if (_Tolinku._initialized) {
|
|
@@ -1191,6 +1311,8 @@ function TolinkuMessages({
|
|
|
1191
1311
|
exports.Tolinku = Tolinku;
|
|
1192
1312
|
exports.TolinkuError = TolinkuError;
|
|
1193
1313
|
exports.TolinkuMessages = TolinkuMessages;
|
|
1314
|
+
exports.getInstallReferrerToken = getInstallReferrerToken;
|
|
1194
1315
|
exports.isSafeUrl = isSafeUrl;
|
|
1316
|
+
exports.parseInstallReferrer = parseInstallReferrer;
|
|
1195
1317
|
//# sourceMappingURL=index.js.map
|
|
1196
1318
|
//# sourceMappingURL=index.js.map
|