@attryio/react-native 0.1.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 +22 -0
- package/android/src/main/java/io/attry/reactnative/AttryInstallReferrerModule.kt +47 -0
- package/android/src/main/java/io/attry/reactnative/AttryPackage.kt +16 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +362 -0
- package/ios/AttryAppleAds.m +5 -0
- package/ios/AttryAppleAds.swift +31 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @attryio/react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK for Attry mobile attribution.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createAttryReactNative } from "@attryio/react-native";
|
|
7
|
+
|
|
8
|
+
const attry = await createAttryReactNative({
|
|
9
|
+
appId: "457064853",
|
|
10
|
+
apiKey: "attry_live_..."
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
await attry.track("paywall_viewed", {
|
|
14
|
+
properties: {
|
|
15
|
+
plan: "pro"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await attry.revenue("subscription_started", 1999, "USD", "pro_monthly");
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The SDK auto-tracks first SDK install, app opens, sessions, foreground/background transitions, deep link opens, Apple AdServices tokens on iOS, and Google Play Install Referrer on Android when the native modules are available.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package io.attry.reactnative
|
|
2
|
+
|
|
3
|
+
import com.android.installreferrer.api.InstallReferrerClient
|
|
4
|
+
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
5
|
+
import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
9
|
+
import com.facebook.react.bridge.ReactMethod
|
|
10
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
11
|
+
|
|
12
|
+
class AttryInstallReferrerModule(private val reactContext: ReactApplicationContext) :
|
|
13
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
14
|
+
override fun getName(): String = "AttryInstallReferrer"
|
|
15
|
+
|
|
16
|
+
@ReactMethod
|
|
17
|
+
fun getInstallReferrer(promise: Promise) {
|
|
18
|
+
val client = InstallReferrerClient.newBuilder(reactContext).build()
|
|
19
|
+
client.startConnection(object : InstallReferrerStateListener {
|
|
20
|
+
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
|
21
|
+
if (responseCode != InstallReferrerResponse.OK) {
|
|
22
|
+
client.endConnection()
|
|
23
|
+
promise.resolve(null)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
val response = client.installReferrer
|
|
29
|
+
val payload = WritableNativeMap()
|
|
30
|
+
payload.putString("installReferrer", response.installReferrer)
|
|
31
|
+
payload.putDouble("referrerClickTimestampSeconds", response.referrerClickTimestampSeconds.toDouble())
|
|
32
|
+
payload.putDouble("installBeginTimestampSeconds", response.installBeginTimestampSeconds.toDouble())
|
|
33
|
+
payload.putBoolean("googlePlayInstantParam", response.googlePlayInstantParam)
|
|
34
|
+
promise.resolve(payload)
|
|
35
|
+
} catch (error: Exception) {
|
|
36
|
+
promise.reject("attry_install_referrer_error", "Unable to read Google Play Install Referrer.", error)
|
|
37
|
+
} finally {
|
|
38
|
+
client.endConnection()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onInstallReferrerServiceDisconnected() {
|
|
43
|
+
promise.resolve(null)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package io.attry.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class AttryPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(AttryInstallReferrerModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Attry, parseAttryUrl, type AttryConfig, type AttryContext, type AttryStorage } from "@attryio/sdk-core";
|
|
2
|
+
type ReactNativeLike = {
|
|
3
|
+
AppState?: {
|
|
4
|
+
currentState?: string;
|
|
5
|
+
addEventListener?: (event: "change", listener: (state: string) => void) => {
|
|
6
|
+
remove?: () => void;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
Dimensions?: {
|
|
10
|
+
get?: (dimension: "window" | "screen") => {
|
|
11
|
+
width?: number;
|
|
12
|
+
height?: number;
|
|
13
|
+
scale?: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
I18nManager?: {
|
|
17
|
+
localeIdentifier?: string;
|
|
18
|
+
};
|
|
19
|
+
Linking?: {
|
|
20
|
+
getInitialURL?: () => Promise<string | null>;
|
|
21
|
+
addEventListener?: (event: "url", listener: (payload: {
|
|
22
|
+
url: string;
|
|
23
|
+
}) => void) => {
|
|
24
|
+
remove?: () => void;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
NativeModules?: {
|
|
28
|
+
AttryAppleAds?: {
|
|
29
|
+
attributionToken: () => Promise<string | null>;
|
|
30
|
+
};
|
|
31
|
+
AttryInstallReferrer?: {
|
|
32
|
+
getInstallReferrer: () => Promise<InstallReferrerPayload | null>;
|
|
33
|
+
};
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
Platform?: {
|
|
37
|
+
OS?: "ios" | "android" | string;
|
|
38
|
+
Version?: string | number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export interface InstallReferrerPayload {
|
|
42
|
+
installReferrer?: string;
|
|
43
|
+
referrerClickTimestampSeconds?: number;
|
|
44
|
+
installBeginTimestampSeconds?: number;
|
|
45
|
+
googlePlayInstantParam?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface ReactNativeAttryConfig extends Omit<AttryConfig, "platform" | "storage"> {
|
|
48
|
+
storage?: AttryConfig["storage"];
|
|
49
|
+
autoTrackDeepLinks?: boolean;
|
|
50
|
+
autoTrackLifecycleEvents?: boolean;
|
|
51
|
+
autoCollectInstallAttribution?: boolean;
|
|
52
|
+
sessionTimeoutMs?: number;
|
|
53
|
+
}
|
|
54
|
+
export { parseAttryUrl };
|
|
55
|
+
export declare function createAttryReactNative(config: ReactNativeAttryConfig): Promise<Attry>;
|
|
56
|
+
export declare function collectReactNativeContext(rn?: ReactNativeLike | undefined, configuredContext?: AttryConfig["context"]): Promise<AttryContext>;
|
|
57
|
+
export declare function attachReactNativeLifecycleTracking(client: Attry, rn: ReactNativeLike | undefined, storage: AttryStorage | undefined, options: {
|
|
58
|
+
appId: string;
|
|
59
|
+
sessionTimeoutMs?: number;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
export declare function attachDeepLinkTracking(client: Attry, rn?: ReactNativeLike | undefined): Promise<void>;
|
|
62
|
+
export declare function collectInstallAttribution(client: Attry, rn?: ReactNativeLike | undefined): Promise<void>;
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { Attry, MemoryStorage, parseAttryUrl } from "@attryio/sdk-core";
|
|
2
|
+
const SDK_NAME = "attry-react-native";
|
|
3
|
+
const SDK_VERSION = "0.1.0";
|
|
4
|
+
export { parseAttryUrl };
|
|
5
|
+
export async function createAttryReactNative(config) {
|
|
6
|
+
const rn = getReactNative();
|
|
7
|
+
const platform = rn?.Platform?.OS === "ios" || rn?.Platform?.OS === "android"
|
|
8
|
+
? rn.Platform.OS
|
|
9
|
+
: "unknown";
|
|
10
|
+
const storage = config.storage ?? createAsyncStorageAdapter() ?? new MemoryStorage();
|
|
11
|
+
const context = await collectReactNativeContext(rn, config.context);
|
|
12
|
+
const client = await new Attry({
|
|
13
|
+
...config,
|
|
14
|
+
platform,
|
|
15
|
+
storage,
|
|
16
|
+
context
|
|
17
|
+
}).init();
|
|
18
|
+
if (config.autoCollectInstallAttribution ?? true) {
|
|
19
|
+
await collectInstallAttribution(client, rn);
|
|
20
|
+
}
|
|
21
|
+
if (config.autoTrackLifecycleEvents ?? true) {
|
|
22
|
+
await attachReactNativeLifecycleTracking(client, rn, storage, {
|
|
23
|
+
appId: config.appId,
|
|
24
|
+
sessionTimeoutMs: config.sessionTimeoutMs
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (config.autoTrackDeepLinks ?? true) {
|
|
28
|
+
await attachDeepLinkTracking(client, rn);
|
|
29
|
+
}
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
export async function collectReactNativeContext(rn = getReactNative(), configuredContext = {}) {
|
|
33
|
+
const modules = rn?.NativeModules;
|
|
34
|
+
const deviceInfo = modules?.RNDeviceInfo;
|
|
35
|
+
const expoConstants = modules?.ExpoConstants ?? modules?.ExponentConstants;
|
|
36
|
+
const platformConstants = modules?.PlatformConstants;
|
|
37
|
+
const settings = modules?.SettingsManager;
|
|
38
|
+
const screen = rn?.Dimensions?.get?.("screen");
|
|
39
|
+
const locale = configuredContext?.locale ??
|
|
40
|
+
readNestedString(settings?.settings, ["AppleLocale"]) ??
|
|
41
|
+
readFirstString(readNestedUnknown(settings?.settings, ["AppleLanguages"])) ??
|
|
42
|
+
readNestedString(deviceInfo, ["deviceLocale"]) ??
|
|
43
|
+
rn?.I18nManager?.localeIdentifier;
|
|
44
|
+
return compactContext({
|
|
45
|
+
sdkName: SDK_NAME,
|
|
46
|
+
sdkVersion: SDK_VERSION,
|
|
47
|
+
appName: configuredContext?.appName ??
|
|
48
|
+
(await readMaybeString(deviceInfo?.appName)) ??
|
|
49
|
+
readNestedString(expoConstants, ["expoConfig", "name"]) ??
|
|
50
|
+
readNestedString(expoConstants, ["manifest", "name"]),
|
|
51
|
+
appIdentifier: configuredContext?.appIdentifier ??
|
|
52
|
+
(await readMaybeString(deviceInfo?.bundleId)) ??
|
|
53
|
+
(await readMaybeString(deviceInfo?.getBundleId)) ??
|
|
54
|
+
readNestedString(expoConstants, ["expoConfig", "ios", "bundleIdentifier"]) ??
|
|
55
|
+
readNestedString(expoConstants, [
|
|
56
|
+
"expoConfig",
|
|
57
|
+
"android",
|
|
58
|
+
"package"
|
|
59
|
+
]),
|
|
60
|
+
appVersion: configuredContext?.appVersion ??
|
|
61
|
+
(await readMaybeString(deviceInfo?.appVersion)) ??
|
|
62
|
+
(await readMaybeString(deviceInfo?.version)) ??
|
|
63
|
+
(await readMaybeString(deviceInfo?.getVersion)) ??
|
|
64
|
+
readNestedString(expoConstants, ["expoConfig", "version"]) ??
|
|
65
|
+
readNestedString(expoConstants, ["manifest", "version"]) ??
|
|
66
|
+
readNestedString(expoConstants, [
|
|
67
|
+
"manifest2",
|
|
68
|
+
"extra",
|
|
69
|
+
"expoClient",
|
|
70
|
+
"version"
|
|
71
|
+
]),
|
|
72
|
+
appBuild: configuredContext?.appBuild ??
|
|
73
|
+
(await readMaybeString(deviceInfo?.buildNumber)) ??
|
|
74
|
+
(await readMaybeString(deviceInfo?.getBuildNumber)) ??
|
|
75
|
+
readNestedString(expoConstants, ["expoConfig", "ios", "buildNumber"]) ??
|
|
76
|
+
readNestedString(expoConstants, ["expoConfig", "android", "versionCode"]),
|
|
77
|
+
osVersion: rn?.Platform?.Version ? String(rn.Platform.Version) : undefined,
|
|
78
|
+
systemName: configuredContext?.systemName ??
|
|
79
|
+
(await readMaybeString(deviceInfo?.systemName)) ??
|
|
80
|
+
(rn?.Platform?.OS ? String(rn.Platform.OS) : undefined),
|
|
81
|
+
deviceModel: configuredContext?.deviceModel ??
|
|
82
|
+
(await readMaybeString(deviceInfo?.model)) ??
|
|
83
|
+
(await readMaybeString(deviceInfo?.getModel)) ??
|
|
84
|
+
readNestedString(platformConstants, ["Model"]) ??
|
|
85
|
+
readNestedString(platformConstants, ["model"]),
|
|
86
|
+
deviceName: configuredContext?.deviceName ??
|
|
87
|
+
(await readMaybeString(deviceInfo?.deviceName)) ??
|
|
88
|
+
(await readMaybeString(deviceInfo?.getDeviceName)),
|
|
89
|
+
deviceBrand: configuredContext?.deviceBrand ??
|
|
90
|
+
(await readMaybeString(deviceInfo?.brand)) ??
|
|
91
|
+
(await readMaybeString(deviceInfo?.getBrand)),
|
|
92
|
+
deviceManufacturer: configuredContext?.deviceManufacturer ??
|
|
93
|
+
(await readMaybeString(deviceInfo?.manufacturer)) ??
|
|
94
|
+
(await readMaybeString(deviceInfo?.getManufacturer)),
|
|
95
|
+
deviceType: configuredContext?.deviceType ??
|
|
96
|
+
(await readMaybeString(deviceInfo?.deviceType)) ??
|
|
97
|
+
(await readMaybeString(deviceInfo?.getDeviceType)),
|
|
98
|
+
isTablet: configuredContext?.isTablet ??
|
|
99
|
+
(await readMaybeBoolean(deviceInfo?.isTablet)) ??
|
|
100
|
+
(await readMaybeBoolean(deviceInfo?.getDeviceTypeIsTablet)),
|
|
101
|
+
isEmulator: configuredContext?.isEmulator ??
|
|
102
|
+
(await readMaybeBoolean(deviceInfo?.isEmulator)),
|
|
103
|
+
screenWidth: screen?.width,
|
|
104
|
+
screenHeight: screen?.height,
|
|
105
|
+
screenScale: screen?.scale,
|
|
106
|
+
locale,
|
|
107
|
+
language: configuredContext?.language ??
|
|
108
|
+
(typeof locale === "string" ? locale.split(/[-_]/)[0] : undefined),
|
|
109
|
+
timezone: configuredContext?.timezone ??
|
|
110
|
+
safeTimezone() ??
|
|
111
|
+
(await readMaybeString(deviceInfo?.timezone)) ??
|
|
112
|
+
(await readMaybeString(deviceInfo?.getTimezone)),
|
|
113
|
+
country: configuredContext?.country,
|
|
114
|
+
countryCode: configuredContext?.countryCode ??
|
|
115
|
+
readCountryCodeFromLocale(typeof locale === "string" ? locale : undefined),
|
|
116
|
+
region: configuredContext?.region,
|
|
117
|
+
city: configuredContext?.city,
|
|
118
|
+
carrier: configuredContext?.carrier ??
|
|
119
|
+
(await readMaybeString(deviceInfo?.carrier)) ??
|
|
120
|
+
(await readMaybeString(deviceInfo?.getCarrier)),
|
|
121
|
+
networkType: configuredContext?.networkType ??
|
|
122
|
+
(await readMaybeString(deviceInfo?.networkType)) ??
|
|
123
|
+
(await readMaybeString(deviceInfo?.getNetworkType)),
|
|
124
|
+
...configuredContext
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export async function attachReactNativeLifecycleTracking(client, rn = getReactNative(), storage = new MemoryStorage(), options) {
|
|
128
|
+
const installKey = `attry.${options.appId}.install_tracked`;
|
|
129
|
+
const sessionStartedAt = Date.now();
|
|
130
|
+
const sessionId = `ses_${sessionStartedAt.toString(36)}`;
|
|
131
|
+
if (!(await storage.getItem(installKey))) {
|
|
132
|
+
await storage.setItem(installKey, new Date().toISOString());
|
|
133
|
+
await client.track("install", {
|
|
134
|
+
properties: {
|
|
135
|
+
auto: true,
|
|
136
|
+
firstSdkOpen: true
|
|
137
|
+
},
|
|
138
|
+
context: {
|
|
139
|
+
sessionId
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
await client.track("app_open", {
|
|
144
|
+
properties: {
|
|
145
|
+
auto: true,
|
|
146
|
+
appState: rn?.AppState?.currentState ?? "active"
|
|
147
|
+
},
|
|
148
|
+
context: {
|
|
149
|
+
sessionId
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
await client.track("session_started", {
|
|
153
|
+
properties: {
|
|
154
|
+
auto: true
|
|
155
|
+
},
|
|
156
|
+
context: {
|
|
157
|
+
sessionId
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
let active = rn?.AppState?.currentState !== "background";
|
|
161
|
+
let lastActiveAt = sessionStartedAt;
|
|
162
|
+
const sessionTimeoutMs = options.sessionTimeoutMs ?? 30 * 60 * 1000;
|
|
163
|
+
rn?.AppState?.addEventListener?.("change", (state) => {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
if (state === "active" && !active) {
|
|
166
|
+
active = true;
|
|
167
|
+
lastActiveAt = now;
|
|
168
|
+
void client.track("app_foreground", {
|
|
169
|
+
properties: {
|
|
170
|
+
auto: true
|
|
171
|
+
},
|
|
172
|
+
context: {
|
|
173
|
+
sessionId
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if ((state === "background" || state === "inactive") && active) {
|
|
179
|
+
active = false;
|
|
180
|
+
const durationMs = Math.max(0, now - lastActiveAt);
|
|
181
|
+
void client.track("app_background", {
|
|
182
|
+
properties: {
|
|
183
|
+
auto: true,
|
|
184
|
+
durationMs
|
|
185
|
+
},
|
|
186
|
+
context: {
|
|
187
|
+
sessionId
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
if (durationMs >= sessionTimeoutMs || state === "background") {
|
|
191
|
+
void client.track("session_ended", {
|
|
192
|
+
properties: {
|
|
193
|
+
auto: true,
|
|
194
|
+
durationMs: Math.max(0, now - sessionStartedAt)
|
|
195
|
+
},
|
|
196
|
+
context: {
|
|
197
|
+
sessionId
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
void client.flush();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
export async function attachDeepLinkTracking(client, rn = getReactNative()) {
|
|
206
|
+
const initialUrl = await rn?.Linking?.getInitialURL?.();
|
|
207
|
+
if (initialUrl) {
|
|
208
|
+
await trackDeepLinkOpen(client, initialUrl, "initial_url", rn);
|
|
209
|
+
}
|
|
210
|
+
rn?.Linking?.addEventListener?.("url", ({ url }) => {
|
|
211
|
+
void trackDeepLinkOpen(client, url, "linking_event", rn);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
export async function collectInstallAttribution(client, rn = getReactNative()) {
|
|
215
|
+
if (rn?.Platform?.OS === "ios") {
|
|
216
|
+
const token = await rn.NativeModules?.AttryAppleAds?.attributionToken?.();
|
|
217
|
+
if (token) {
|
|
218
|
+
const response = await client.submitAppleAdsToken(token);
|
|
219
|
+
await client.track("apple_ads_token_collected", {
|
|
220
|
+
attribution: response?.attribution ?? {
|
|
221
|
+
source: "apple_ads_adservices",
|
|
222
|
+
confidence: "deterministic"
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (rn?.Platform?.OS === "android") {
|
|
229
|
+
const payload = await rn.NativeModules?.AttryInstallReferrer?.getInstallReferrer?.();
|
|
230
|
+
if (payload?.installReferrer) {
|
|
231
|
+
const response = await client.resolveInstall({
|
|
232
|
+
installReferrer: payload.installReferrer
|
|
233
|
+
});
|
|
234
|
+
await client.track("install_referrer_collected", {
|
|
235
|
+
properties: payload,
|
|
236
|
+
attribution: response?.attribution ?? {
|
|
237
|
+
source: "google_play_install_referrer",
|
|
238
|
+
installReferrer: payload.installReferrer,
|
|
239
|
+
confidence: "deterministic"
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function trackDeepLinkOpen(client, url, openType, rn = getReactNative()) {
|
|
246
|
+
const parsed = parseAttryUrl(url);
|
|
247
|
+
const openedFromUniversalLink = rn?.Platform?.OS === "ios";
|
|
248
|
+
const openedFromAndroidAppLink = rn?.Platform?.OS === "android";
|
|
249
|
+
const resolved = await client.resolveDeepLink({
|
|
250
|
+
url,
|
|
251
|
+
openedFromUniversalLink,
|
|
252
|
+
openedFromAndroidAppLink
|
|
253
|
+
});
|
|
254
|
+
const resolvedLink = typeof resolved?.link === "object" && resolved.link
|
|
255
|
+
? resolved.link
|
|
256
|
+
: undefined;
|
|
257
|
+
if (parsed.clickId) {
|
|
258
|
+
await client.resolveInstall({
|
|
259
|
+
attryClickId: parsed.clickId,
|
|
260
|
+
openedFromUniversalLink,
|
|
261
|
+
openedFromAndroidAppLink
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
await client.track("deep_link_opened", {
|
|
265
|
+
properties: {
|
|
266
|
+
openType,
|
|
267
|
+
url: parsed.url,
|
|
268
|
+
host: parsed.host,
|
|
269
|
+
path: parsed.path,
|
|
270
|
+
destinationPath: typeof resolvedLink?.destinationPath === "string"
|
|
271
|
+
? resolvedLink.destinationPath
|
|
272
|
+
: undefined,
|
|
273
|
+
linkData: resolvedLink?.data &&
|
|
274
|
+
typeof resolvedLink.data === "object" &&
|
|
275
|
+
!Array.isArray(resolvedLink.data)
|
|
276
|
+
? resolvedLink.data
|
|
277
|
+
: undefined
|
|
278
|
+
},
|
|
279
|
+
attribution: resolved?.attribution ?? {
|
|
280
|
+
source: openedFromAndroidAppLink ? "app_link" : "universal_link",
|
|
281
|
+
clickId: parsed.clickId,
|
|
282
|
+
campaign: parsed.campaign,
|
|
283
|
+
medium: parsed.medium,
|
|
284
|
+
referrerSource: parsed.source,
|
|
285
|
+
confidence: "deterministic"
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async function readMaybeString(value) {
|
|
290
|
+
try {
|
|
291
|
+
const resolved = typeof value === "function" ? await value() : value;
|
|
292
|
+
return typeof resolved === "string" && resolved.trim()
|
|
293
|
+
? resolved.trim()
|
|
294
|
+
: undefined;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function readMaybeBoolean(value) {
|
|
301
|
+
try {
|
|
302
|
+
const resolved = typeof value === "function" ? await value() : value;
|
|
303
|
+
return typeof resolved === "boolean" ? resolved : undefined;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function readNestedString(record, path) {
|
|
310
|
+
const value = readNestedUnknown(record, path);
|
|
311
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
312
|
+
}
|
|
313
|
+
function readNestedUnknown(record, path) {
|
|
314
|
+
let current = record;
|
|
315
|
+
for (const key of path) {
|
|
316
|
+
if (!current || typeof current !== "object" || !(key in current)) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
current = current[key];
|
|
320
|
+
}
|
|
321
|
+
return current;
|
|
322
|
+
}
|
|
323
|
+
function readFirstString(value) {
|
|
324
|
+
return Array.isArray(value) && typeof value[0] === "string"
|
|
325
|
+
? value[0]
|
|
326
|
+
: undefined;
|
|
327
|
+
}
|
|
328
|
+
function compactContext(context) {
|
|
329
|
+
return Object.fromEntries(Object.entries(context).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
330
|
+
}
|
|
331
|
+
function readCountryCodeFromLocale(locale) {
|
|
332
|
+
const normalized = locale?.replace("_", "-");
|
|
333
|
+
const country = normalized?.split("-")[1];
|
|
334
|
+
return country && country.length === 2 ? country.toUpperCase() : undefined;
|
|
335
|
+
}
|
|
336
|
+
function safeTimezone() {
|
|
337
|
+
try {
|
|
338
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function getReactNative() {
|
|
345
|
+
try {
|
|
346
|
+
const dynamicRequire = eval("require");
|
|
347
|
+
return dynamicRequire("react-native");
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function createAsyncStorageAdapter() {
|
|
354
|
+
try {
|
|
355
|
+
const dynamicRequire = eval("require");
|
|
356
|
+
const asyncStorage = dynamicRequire("@react-native-async-storage/async-storage").default;
|
|
357
|
+
return asyncStorage;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
#if canImport(AdServices)
|
|
4
|
+
import AdServices
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
@objc(AttryAppleAds)
|
|
8
|
+
class AttryAppleAds: NSObject {
|
|
9
|
+
@objc
|
|
10
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@objc(attributionToken:rejecter:)
|
|
15
|
+
func attributionToken(resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
16
|
+
#if canImport(AdServices)
|
|
17
|
+
if #available(iOS 14.3, *) {
|
|
18
|
+
do {
|
|
19
|
+
let token = try AAAttribution.attributionToken()
|
|
20
|
+
resolve(token)
|
|
21
|
+
} catch {
|
|
22
|
+
reject("attry_adservices_error", "Unable to read Apple AdServices attribution token.", error)
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
resolve(nil)
|
|
26
|
+
}
|
|
27
|
+
#else
|
|
28
|
+
resolve(nil)
|
|
29
|
+
#endif
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@attryio/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"ios",
|
|
18
|
+
"android",
|
|
19
|
+
"README.md",
|
|
20
|
+
"package.json"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@attryio/sdk-core": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react-native": ">=0.72"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|