@tolinku/react-native-sdk 0.3.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 +24 -0
- 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 +67 -3
- package/dist/index.d.ts +67 -3
- package/dist/index.js +115 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +115 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -149,6 +149,30 @@ const link = await Tolinku.deferred.claimBySignals({
|
|
|
149
149
|
});
|
|
150
150
|
```
|
|
151
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
|
+
|
|
152
176
|
`appspaceId` is your Appspace ID, not your subdomain or slug. Copy it from the dashboard
|
|
153
177
|
under **Integrate** or **Settings**. It looks like `64f0a1b2c3d4e5f60718`.
|
|
154
178
|
|
|
@@ -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
|
@@ -319,13 +319,68 @@ declare class Referrals {
|
|
|
319
319
|
}>;
|
|
320
320
|
}
|
|
321
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
|
+
|
|
322
343
|
declare class Deferred {
|
|
323
344
|
private client;
|
|
324
345
|
constructor(client: HttpClient);
|
|
325
346
|
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
326
|
-
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;
|
|
327
372
|
/** Claim a deferred deep link by device signal matching */
|
|
328
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;
|
|
329
384
|
}
|
|
330
385
|
|
|
331
386
|
/**
|
|
@@ -338,7 +393,7 @@ declare class Deferred {
|
|
|
338
393
|
* await Tolinku.track('signup', { source: 'onboarding' });
|
|
339
394
|
*/
|
|
340
395
|
declare class Tolinku {
|
|
341
|
-
static readonly VERSION = "0.
|
|
396
|
+
static readonly VERSION = "0.4.0";
|
|
342
397
|
private static client;
|
|
343
398
|
private static analyticsInstance;
|
|
344
399
|
private static ecommerceInstance;
|
|
@@ -352,6 +407,15 @@ declare class Tolinku {
|
|
|
352
407
|
* If init() is called a second time without calling destroy() first,
|
|
353
408
|
* a warning is logged and the existing instance is returned.
|
|
354
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;
|
|
355
419
|
static init(config: TolinkuConfig): void;
|
|
356
420
|
/** Check whether the SDK has been initialized. */
|
|
357
421
|
static isConfigured(): boolean;
|
|
@@ -417,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
|
|
|
417
481
|
*/
|
|
418
482
|
declare function isSafeUrl(url: string): boolean;
|
|
419
483
|
|
|
420
|
-
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
|
@@ -319,13 +319,68 @@ declare class Referrals {
|
|
|
319
319
|
}>;
|
|
320
320
|
}
|
|
321
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
|
+
|
|
322
343
|
declare class Deferred {
|
|
323
344
|
private client;
|
|
324
345
|
constructor(client: HttpClient);
|
|
325
346
|
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
326
|
-
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;
|
|
327
372
|
/** Claim a deferred deep link by device signal matching */
|
|
328
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;
|
|
329
384
|
}
|
|
330
385
|
|
|
331
386
|
/**
|
|
@@ -338,7 +393,7 @@ declare class Deferred {
|
|
|
338
393
|
* await Tolinku.track('signup', { source: 'onboarding' });
|
|
339
394
|
*/
|
|
340
395
|
declare class Tolinku {
|
|
341
|
-
static readonly VERSION = "0.
|
|
396
|
+
static readonly VERSION = "0.4.0";
|
|
342
397
|
private static client;
|
|
343
398
|
private static analyticsInstance;
|
|
344
399
|
private static ecommerceInstance;
|
|
@@ -352,6 +407,15 @@ declare class Tolinku {
|
|
|
352
407
|
* If init() is called a second time without calling destroy() first,
|
|
353
408
|
* a warning is logged and the existing instance is returned.
|
|
354
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;
|
|
355
419
|
static init(config: TolinkuConfig): void;
|
|
356
420
|
/** Check whether the SDK has been initialized. */
|
|
357
421
|
static isConfigured(): boolean;
|
|
@@ -417,4 +481,4 @@ declare function TolinkuMessages({ trigger, triggerValue, onDismiss, onButtonPre
|
|
|
417
481
|
*/
|
|
418
482
|
declare function isSafeUrl(url: string): boolean;
|
|
419
483
|
|
|
420
|
-
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,32 +630,126 @@ 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
751
|
const resolvedLanguage = options.language || (typeof Intl !== "undefined" && typeof Intl.DateTimeFormat === "function" ? Intl.DateTimeFormat().resolvedOptions().locale : void 0) || "en";
|
|
658
|
-
|
|
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,
|
|
@@ -665,20 +759,21 @@ var Deferred = class {
|
|
|
665
759
|
device_pixel_ratio: options.devicePixelRatio || reactNative.PixelRatio.get(),
|
|
666
760
|
os_version: options.osVersion || String(reactNative.Platform.Version)
|
|
667
761
|
});
|
|
762
|
+
return { link, settled: true };
|
|
668
763
|
} catch (err) {
|
|
669
764
|
const status = err?.statusCode ?? err?.status;
|
|
670
765
|
if (status === 404) {
|
|
671
766
|
debugWarn("Deferred claimBySignals: no match for this device.");
|
|
672
|
-
return null;
|
|
767
|
+
return { link: null, settled: true };
|
|
673
768
|
}
|
|
674
769
|
if (status === 403) {
|
|
675
770
|
console.warn(
|
|
676
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}`
|
|
677
772
|
);
|
|
678
|
-
return null;
|
|
773
|
+
return { link: null, settled: false };
|
|
679
774
|
}
|
|
680
775
|
debugWarn(`Deferred claimBySignals failed: ${err.message}`);
|
|
681
|
-
return null;
|
|
776
|
+
return { link: null, settled: false };
|
|
682
777
|
}
|
|
683
778
|
}
|
|
684
779
|
};
|
|
@@ -768,6 +863,17 @@ var _Tolinku = class _Tolinku {
|
|
|
768
863
|
* If init() is called a second time without calling destroy() first,
|
|
769
864
|
* a warning is logged and the existing instance is returned.
|
|
770
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
|
+
}
|
|
771
877
|
static init(config) {
|
|
772
878
|
if (!config.apiKey) throw new Error("Tolinku: apiKey is required");
|
|
773
879
|
if (_Tolinku._initialized) {
|
|
@@ -1205,6 +1311,8 @@ function TolinkuMessages({
|
|
|
1205
1311
|
exports.Tolinku = Tolinku;
|
|
1206
1312
|
exports.TolinkuError = TolinkuError;
|
|
1207
1313
|
exports.TolinkuMessages = TolinkuMessages;
|
|
1314
|
+
exports.getInstallReferrerToken = getInstallReferrerToken;
|
|
1208
1315
|
exports.isSafeUrl = isSafeUrl;
|
|
1316
|
+
exports.parseInstallReferrer = parseInstallReferrer;
|
|
1209
1317
|
//# sourceMappingURL=index.js.map
|
|
1210
1318
|
//# sourceMappingURL=index.js.map
|