appsprint-react-native 0.2.0 → 1.0.4

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.
Files changed (44) hide show
  1. package/README.md +173 -68
  2. package/android/build.gradle +4 -2
  3. package/android/libs/appsprint-sdk.aar +0 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/kotlin/com/appsprint/AppSprintBridgeModule.kt +125 -29
  6. package/appsprint-react-native.podspec +6 -1
  7. package/ios/AppSprintBridge.m +6 -0
  8. package/ios/AppSprintBridge.swift +92 -16
  9. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/AppSprintSDK +0 -0
  10. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Info.plist +0 -0
  11. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios.abi.json +4757 -952
  12. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +102 -30
  13. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +102 -30
  14. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  15. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios.swiftinterface +102 -30
  16. package/ios/AppSprintSDK.xcframework/ios-arm64/AppSprintSDK.framework/PrivacyInfo.xcprivacy +91 -0
  17. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/AppSprintSDK +0 -0
  18. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Info.plist +0 -0
  19. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +4757 -952
  20. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +102 -30
  21. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +102 -30
  22. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  23. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +102 -30
  24. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +4757 -952
  25. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +102 -30
  26. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +102 -30
  27. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  28. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/Modules/AppSprintSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +102 -30
  29. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/PrivacyInfo.xcprivacy +91 -0
  30. package/ios/AppSprintSDK.xcframework/ios-arm64_x86_64-simulator/AppSprintSDK.framework/_CodeSignature/CodeResources +1 -1
  31. package/lib/commonjs/AppSprint.js +40 -4
  32. package/lib/commonjs/NativeAppSprint.js +15 -3
  33. package/lib/module/AppSprint.js +40 -4
  34. package/lib/module/NativeAppSprint.js +15 -3
  35. package/lib/typescript/AppSprint.d.ts +6 -4
  36. package/lib/typescript/index.d.ts +1 -1
  37. package/lib/typescript/types.d.ts +53 -7
  38. package/package.json +5 -2
  39. package/plugin/build/index.js +37 -10
  40. package/plugin/src/index.ts +44 -1
  41. package/src/AppSprint.ts +85 -10
  42. package/src/NativeAppSprint.ts +15 -3
  43. package/src/index.ts +1 -0
  44. package/src/types.ts +87 -7
package/README.md CHANGED
@@ -1,22 +1,41 @@
1
- # appsprint-react-native
1
+ # AppSprint for React Native
2
2
 
3
- AppSprint mobile attribution SDK for React Native. It tracks installs, attribution, lifecycle events, custom events, and revenue events, with local event queueing for transient failures.
3
+ Mobile attribution and event tracking for React Native, with native iOS and Android SDKs bundled inside. Works with bare React Native and Expo. The JS bridge is thin: it forwards calls to the same native engines as our standalone iOS and Android SDKs.
4
4
 
5
- ## Installation
5
+ ## Requirements
6
+
7
+ - React Native 0.71 or later
8
+ - React 18 or later
9
+ - iOS 14.0 or later
10
+ - Android 7.0 (API 24) or later
11
+
12
+ ## Install
6
13
 
7
14
  ```bash
8
15
  npm install appsprint-react-native
9
16
  ```
10
17
 
18
+ or
19
+
20
+ ```bash
21
+ yarn add appsprint-react-native
22
+ ```
23
+
11
24
  ### iOS
12
25
 
13
26
  ```bash
14
27
  cd ios && pod install
15
28
  ```
16
29
 
17
- ## Expo config plugin
30
+ The native pod is vendored inside the package, so no extra repository setup is needed.
31
+
32
+ ### Android
33
+
34
+ Auto-linking handles the Android side. The package's manifest declares `INTERNET` and `com.google.android.gms.permission.AD_ID`, which merge into your app at build time.
35
+
36
+ ### Expo
18
37
 
19
- If you use Expo prebuild, add the plugin to your app config:
38
+ If you use Expo prebuild, add the config plugin to `app.json` or `app.config.js`:
20
39
 
21
40
  ```json
22
41
  {
@@ -24,23 +43,23 @@ If you use Expo prebuild, add the plugin to your app config:
24
43
  [
25
44
  "appsprint-react-native",
26
45
  {
27
- "trackingDescription": "This identifier will be used to deliver personalized ads to you."
46
+ "trackingDescription": "This identifier helps us deliver personalized ads."
28
47
  }
29
48
  ]
30
49
  ]
31
50
  }
32
51
  ```
33
52
 
34
- Plugin options:
53
+ The plugin injects `NSUserTrackingUsageDescription` on iOS and the Android permissions during prebuild.
35
54
 
36
- | Option | Type | Description | Default |
55
+ | Plugin option | Type | Description | Default |
37
56
  |---|---|---|---|
38
- | `trackingDescription` | `string` | ATT permission dialog text for `NSUserTrackingUsageDescription` | `"This identifier will be used to deliver personalized ads to you."` |
39
- | `advertisingAttributionEndpoint` | `string` | Sets `NSAdvertisingAttributionReportEndpoint` | |
57
+ | `trackingDescription` | `string` | Text for the ATT permission prompt. | `"This identifier will be used to deliver personalized ads to you."` |
58
+ | `advertisingAttributionEndpoint` | `string` | Sets `NSAdvertisingAttributionReportEndpoint`. | none |
40
59
 
41
- ## Quick start
60
+ ## Configure
42
61
 
43
- Initialize the SDK as early as possible in app startup:
62
+ Call `configure` once at app startup. It returns a promise that resolves after local state is restored; install registration runs in the background:
44
63
 
45
64
  ```tsx
46
65
  import { AppSprint } from "appsprint-react-native";
@@ -50,28 +69,48 @@ await AppSprint.configure({
50
69
  });
51
70
  ```
52
71
 
53
- ### Configuration
72
+ A typical app calls this from `App.tsx` (or `app/_layout.tsx` on Expo Router):
54
73
 
55
- | Option | Type | Required | Default |
56
- |---|---|---|---|
57
- | `apiKey` | `string` | Yes | — |
58
- | `apiUrl` | `string` | No | `https://api.appsprint.app` |
59
- | `enableAppleAdsAttribution` | `boolean` | No | `true` |
60
- | `isDebug` | `boolean` | No | `false` |
61
- | `logLevel` | `0 \| 1 \| 2 \| 3` | No | `2` |
62
- | `customerUserId` | `string \| null` | No | `null` |
74
+ ```tsx
75
+ import { useEffect } from "react";
76
+ import { AppSprint, NativeAppSprint } from "appsprint-react-native";
77
+
78
+ export default function App() {
79
+ useEffect(() => {
80
+ (async () => {
81
+ await AppSprint.configure({ apiKey: "YOUR_API_KEY" });
82
+
83
+ // iOS only. Skipped at runtime on Android.
84
+ await NativeAppSprint.requestTrackingAuthorization();
85
+ })();
86
+ }, []);
63
87
 
64
- Log levels:
88
+ return <RootNavigator />;
89
+ }
90
+ ```
65
91
 
66
- `0 = debug`, `1 = info`, `2 = warn`, `3 = error`
92
+ ### Configuration options
67
93
 
68
- ## Sending events
94
+ | Option | Type | Default | What it does |
95
+ |---|---|---|---|
96
+ | `apiKey` | `string` | required | Your AppSprint app key. |
97
+ | `apiUrl` | `string` | `https://api.appsprint.app` | Override for staging or self-hosted environments. |
98
+ | `endpointBaseUrl` | `string` | alias for `apiUrl` | Accepted for compatibility. |
99
+ | `enableAppleAdsAttribution` | `boolean` | `true` | iOS only. Fetches Apple AdServices at install time. |
100
+ | `customerUserId` | `string \| null` | `null` | Your internal user ID. Persists across launches and replays if the first send fails. |
101
+ | `autoTrackSessions` | `boolean` | `true` | Fires `session_start` on `configure()` and on foreground, debounced to one event per 30 minutes. |
102
+ | `autoRefreshAttribution` | `boolean` | `true` | Refreshes attribution from the backend on `configure()` and on foreground. |
103
+ | `isDebug` | `boolean` | `false` | Forces debug-level logging on the native side. |
104
+ | `logLevel` | `0 \| 1 \| 2 \| 3` | `2` | `0 = debug`, `1 = info`, `2 = warn`, `3 = error`. |
105
+
106
+ ## Track events
69
107
 
70
108
  ```tsx
71
109
  import { AppSprint } from "appsprint-react-native";
72
110
 
73
111
  await AppSprint.sendEvent("login");
74
112
  await AppSprint.sendEvent("sign_up");
113
+
75
114
  await AppSprint.sendEvent("purchase", null, {
76
115
  revenue: 9.99,
77
116
  currency: "USD",
@@ -83,52 +122,65 @@ await AppSprint.sendEvent("custom", "onboarding_step", {
83
122
  });
84
123
  ```
85
124
 
86
- Supported `eventType` values:
125
+ `sendEvent` resolves once the native side has queued the event locally. The actual HTTP send happens on the next flush trigger (foreground, background, or another `sendEvent`).
87
126
 
88
- `login` | `sign_up` | `register` | `purchase` | `subscribe` | `start_trial` | `add_to_cart` | `add_to_wishlist` | `initiate_checkout` | `view_content` | `view_item` | `search` | `share` | `tutorial_complete` | `level_start` | `level_complete` | `custom`
127
+ ### Built-in event types
89
128
 
90
- Notes:
129
+ `session_start`, `login`, `sign_up`, `register`, `purchase`, `subscribe`, `start_trial`, `add_payment_info`, `add_to_cart`, `add_to_wishlist`, `initiate_checkout`, `view_content`, `view_item`, `search`, `share`, `tutorial_complete`, `achieve_level`, `level_start`, `level_complete`, `custom`.
91
130
 
92
- - Use `eventType: "custom"` together with the optional `name` argument for custom event names.
93
- - Revenue fields are accepted through `params.revenue` and `params.currency`.
94
- - If an event cannot be delivered, it is queued locally and retried on the next initialization or explicit flush.
131
+ ### Revenue events
95
132
 
96
- ## Public API
133
+ Pass `revenue` (or `price` as an alias) plus `currency`. Currency must be a 3-letter ISO code; anything else is dropped on the native side before the request goes out.
97
134
 
98
- ### `AppSprint`
135
+ ```tsx
136
+ await AppSprint.sendEvent("subscribe", null, {
137
+ revenue: 4.99,
138
+ currency: "EUR",
139
+ plan: "monthly",
140
+ });
141
+ ```
142
+
143
+ ### Custom events
99
144
 
100
145
  ```tsx
101
- import { AppSprint } from "appsprint-react-native";
146
+ await AppSprint.sendEvent("custom", "level_skip", { level: 12 });
102
147
  ```
103
148
 
104
- Available methods:
149
+ Use the second argument (`name`) to label the event. Keep it stable so your dashboard groups it correctly.
105
150
 
106
- - `configure(config)` initializes the SDK and performs install tracking when needed.
107
- - `sendEvent(eventType, name?, params?)` sends or queues an event.
108
- - `sendTestEvent()` sends a diagnostic event and returns `{ success, message }`.
109
- - `flush()` retries queued events immediately.
110
- - `clearData()` clears cached SDK state and the local event queue.
111
- - `isSdkDisabled()` returns whether the SDK has been disabled because the API key was rejected.
112
- - `setCustomerUserId(userId)` updates the customer user id locally and remotely when possible.
113
- - `getAppSprintId()` returns the cached AppSprint install identifier, if available.
114
- - `getAttribution()` returns the last cached attribution result, if available.
115
- - `enableAppleAdsAttribution()` re-enables Apple Ads attribution in the current runtime config.
116
- - `isInitialized()` reports whether `configure()` completed.
117
- - `destroy()` removes SDK listeners.
151
+ ## Read attribution
118
152
 
119
- ### `NativeAppSprint`
153
+ Once an install registers, attribution is cached on the native side. You can read it any time:
120
154
 
121
155
  ```tsx
122
- import { NativeAppSprint } from "appsprint-react-native";
156
+ const attribution = await AppSprint.getAttribution();
157
+ const appsprintId = await AppSprint.getAppSprintId();
158
+ const params = await AppSprint.getAttributionParams();
159
+ ```
160
+
161
+ `AttributionResult.source` is one of `apple_ads`, `tracking_link`, or `organic`.
162
+
163
+ ### Forward to RevenueCat or Superwall
164
+
165
+ `getAttributionParams()` returns a flat `Record<string, string>` shaped for partner SDKs:
166
+
167
+ ```tsx
168
+ import Purchases from "react-native-purchases";
169
+
170
+ const params = await AppSprint.getAttributionParams();
171
+ Purchases.setAttributes(params);
123
172
  ```
124
173
 
125
- Available methods:
174
+ ### Manual refresh
126
175
 
127
- - `getDeviceInfo()`
128
- - `getAdServicesToken()`
129
- - `requestTrackingAuthorization()`
176
+ If you need the latest server-side resolution (for example after granting ATT mid-session), call `refreshAttribution()`:
130
177
 
131
- Example ATT request on iOS:
178
+ ```tsx
179
+ const updated = await AppSprint.refreshAttribution();
180
+ console.log("source =", updated?.source);
181
+ ```
182
+
183
+ ## App Tracking Transparency (iOS only)
132
184
 
133
185
  ```tsx
134
186
  import { NativeAppSprint } from "appsprint-react-native";
@@ -136,38 +188,91 @@ import { NativeAppSprint } from "appsprint-react-native";
136
188
  const authorized = await NativeAppSprint.requestTrackingAuthorization();
137
189
  ```
138
190
 
139
- ## Attribution
191
+ The helper waits internally for the app to reach foreground-active before showing the system prompt. If you call it during initial mount, it will queue and run when the user gets to your first screen.
140
192
 
141
- The SDK tracks install attribution once an install is registered. You can read the cached values at any time:
193
+ For bare React Native apps, add `NSUserTrackingUsageDescription` to `ios/<App>/Info.plist`. Expo users get this through the config plugin's `trackingDescription` option.
142
194
 
143
- ```tsx
144
- const attribution = await AppSprint.getAttribution();
145
- const appsprintId = await AppSprint.getAppSprintId();
195
+ `NativeAppSprint.requestTrackingAuthorization()` resolves `true` on Android without prompting; ATT is iOS-only.
196
+
197
+ ## Google Advertising ID (Android only)
198
+
199
+ The native Android SDK reads GAID during install registration, off the main thread, honoring Limit Ad Tracking and dropping the all-zero ID. If your app cannot collect advertising IDs (children's apps, regional policies), remove the permission in your host app manifest:
200
+
201
+ ```xml
202
+ <manifest xmlns:tools="http://schemas.android.com/tools" ...>
203
+ <uses-permission
204
+ android:name="com.google.android.gms.permission.AD_ID"
205
+ tools:node="remove" />
206
+ </manifest>
146
207
  ```
147
208
 
148
- `AttributionResult.source` can be:
209
+ ## What happens behind the scenes
149
210
 
150
- `apple_ads` | `fingerprint` | `organic`
211
+ - `configure()` resolves after local-state restore. Install registration runs in the background and retries with backoff on transient failures.
212
+ - Events queue locally on native storage and survive app restarts.
213
+ - iOS uses connectivity-aware networking, so transient offline windows queue inside the OS rather than failing fast.
214
+ - A rejected API key (`401` or `403`) disables the SDK on the native side. Future events drop until `clearData()` is called.
215
+ - Late identity updates (`setCustomerUserId`, iOS Apple Ads opt-in) retry automatically on the next `configure()` or foreground.
151
216
 
152
- ## Offline and retry behavior
217
+ ## Privacy
153
218
 
154
- - The SDK keeps up to `100` queued events in local storage.
155
- - Queued events are flushed after `configure()` and when the app moves to the background.
156
- - Failed flushes keep the unsent events queued for a later retry.
157
- - A rejected API key (`401` or `403`) disables the SDK and drops future events until cached data is cleared.
219
+ The vendored iOS framework ships a `PrivacyInfo.xcprivacy` manifest declaring `UserDefaults` access plus `DeviceID`, `ProductInteraction`, `UserID`, `CoarseLocation`, and `OtherDataTypes` collection, all marked `Tracking: true`, with `api.appsprint.app` listed as a tracking domain.
158
220
 
159
- ## Local development
221
+ For Android, include advertising ID collection, device IDs, app activity, and (if you set `customerUserId`) user ID in your Play Console Data safety answers.
160
222
 
161
- Point the SDK at a non-production backend during development:
223
+ Don't pass raw PII through `params` or `customerUserId`. Both persist to native storage for retry durability. Use hashed or opaque identifiers instead (SHA-256 of an email, RevenueCat or Superwall `app_user_id`, your internal user UUID).
224
+
225
+ ## Local development
162
226
 
163
227
  ```tsx
164
228
  await AppSprint.configure({
165
- apiKey: "YOUR_API_KEY",
229
+ apiKey: "YOUR_DEV_KEY",
166
230
  apiUrl: "http://localhost:3000",
167
231
  isDebug: true,
168
232
  });
169
233
  ```
170
234
 
235
+ On Android emulator, use `http://10.0.2.2:3000` to reach the host machine's localhost.
236
+
237
+ `isDebug: true` raises native log level to `debug`. iOS logs flow into Console.app; Android logs flow into `logcat` under the `AppSprint` tag.
238
+
239
+ ## Public API reference
240
+
241
+ ### `AppSprint`
242
+
243
+ ```tsx
244
+ import { AppSprint } from "appsprint-react-native";
245
+ ```
246
+
247
+ - `configure(config)` initializes the SDK.
248
+ - `sendEvent(eventType, name?, params?)` enqueues an event.
249
+ - `flush()` drains the queue immediately.
250
+ - `refreshAttribution()` fetches the latest attribution from the backend.
251
+ - `setCustomerUserId(userId)` updates the customer user ID.
252
+ - `getAttribution()` returns the cached attribution.
253
+ - `getAttributionParams()` returns the partner-ready payload.
254
+ - `getAppSprintId()` returns the SDK install identifier.
255
+ - `enableAppleAdsAttribution()` re-enables Apple Ads at runtime on iOS; returns `false` on Android.
256
+ - `sendTestEvent()` posts a diagnostic event and resolves to `{ success, message }`.
257
+ - `isInitialized()` reports whether `configure()` resolved.
258
+ - `isSdkDisabled()` reports whether a rejected API key disabled the SDK.
259
+ - `clearData()` wipes local state.
260
+ - `destroy()` removes native lifecycle observers.
261
+
262
+ ### `NativeAppSprint`
263
+
264
+ ```tsx
265
+ import { NativeAppSprint } from "appsprint-react-native";
266
+ ```
267
+
268
+ - `getDeviceInfo()` returns the device fingerprint payload.
269
+ - `getAdServicesToken()` returns Apple's AdServices token on iOS; `null` on Android.
270
+ - `requestTrackingAuthorization()` shows the ATT prompt on iOS; resolves `true` on Android.
271
+
272
+ ## Support
273
+
274
+ Issues and feature requests on the [GitHub repo](https://github.com/getappsprint/appsprint-react-native). Direct support at support@appsprint.app.
275
+
171
276
  ## License
172
277
 
173
278
  MIT
@@ -9,7 +9,7 @@ apply plugin: 'kotlin-android'
9
9
 
10
10
  android {
11
11
  namespace "com.appsprint"
12
- compileSdkVersion safeExtGet('compileSdkVersion', 35)
12
+ compileSdkVersion safeExtGet('compileSdkVersion', 36)
13
13
 
14
14
  defaultConfig {
15
15
  minSdkVersion safeExtGet('minSdkVersion', 24)
@@ -35,5 +35,7 @@ android {
35
35
  dependencies {
36
36
  implementation "com.facebook.react:react-android:+"
37
37
  implementation files('libs/appsprint-sdk.aar')
38
- implementation "androidx.lifecycle:lifecycle-process:2.8.7"
38
+ implementation "androidx.lifecycle:lifecycle-process:2.10.0"
39
+ implementation "com.google.android.gms:play-services-ads-identifier:18.3.0"
40
+ implementation "com.android.installreferrer:installreferrer:2.2"
39
41
  }
Binary file
@@ -1,3 +1,5 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  package="com.appsprint">
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
3
5
  </manifest>
@@ -3,14 +3,31 @@ package com.appsprint
3
3
  import com.appsprint.sdk.AppSprint
4
4
  import com.appsprint.sdk.AppSprintConfig
5
5
  import com.appsprint.sdk.AppSprintEventType
6
+ import com.appsprint.sdk.AppSprintNative
7
+ import com.appsprint.sdk.AttributionResult
8
+ import com.appsprint.sdk.GoogleAdsConsent
9
+ import com.appsprint.sdk.GoogleAdsConsentStatus
6
10
  import com.facebook.react.bridge.*
7
- import kotlin.concurrent.thread
11
+ import java.util.concurrent.ExecutorService
12
+ import java.util.concurrent.Executors
8
13
 
9
14
  class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
10
15
 
11
16
  override fun getName(): String = "AppSprintModule"
12
17
 
18
+ @Volatile private var cachedSdk: AppSprint? = null
19
+ private val bridgeExecutor: ExecutorService = Executors.newSingleThreadExecutor { runnable ->
20
+ Thread(runnable, "AppSprintRNBridge").apply { isDaemon = true }
21
+ }
22
+
13
23
  private fun sdk(): AppSprint {
24
+ cachedSdk?.let { return it }
25
+ return synchronized(this) {
26
+ cachedSdk ?: resolveSdk().also { cachedSdk = it }
27
+ }
28
+ }
29
+
30
+ private fun resolveSdk(): AppSprint {
14
31
  val sdkClass = AppSprint::class.java
15
32
 
16
33
  runCatching {
@@ -24,15 +41,23 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
24
41
  }
25
42
 
26
43
  private fun runAsync(code: String, promise: Promise, block: () -> Unit) {
27
- thread(start = true) {
44
+ bridgeExecutor.execute {
28
45
  try {
29
46
  block()
30
- } catch (e: Exception) {
31
- promise.reject(code, e.message, e)
47
+ } catch (t: Throwable) {
48
+ promise.reject(code, t.message, t)
32
49
  }
33
50
  }
34
51
  }
35
52
 
53
+ private fun resolveSync(code: String, promise: Promise, block: () -> Any?) {
54
+ try {
55
+ promise.resolve(block())
56
+ } catch (t: Throwable) {
57
+ promise.reject(code, t.message, t)
58
+ }
59
+ }
60
+
36
61
  // Core SDK
37
62
 
38
63
  @ReactMethod
@@ -46,14 +71,21 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
46
71
  runAsync("CONFIGURE_ERROR", promise) {
47
72
  val sdkConfig = AppSprintConfig(
48
73
  apiKey = apiKey,
49
- apiUrl = if (config.hasKey("apiUrl")) config.getString("apiUrl") ?: "https://api.appsprint.app" else "https://api.appsprint.app",
74
+ apiUrl = when {
75
+ config.hasKey("apiUrl") -> config.getString("apiUrl") ?: "https://api.appsprint.app"
76
+ config.hasKey("endpointBaseUrl") -> config.getString("endpointBaseUrl") ?: "https://api.appsprint.app"
77
+ else -> "https://api.appsprint.app"
78
+ },
50
79
  enableAppleAdsAttribution = if (config.hasKey("enableAppleAdsAttribution")) config.getBoolean("enableAppleAdsAttribution") else true,
51
80
  isDebug = if (config.hasKey("isDebug")) config.getBoolean("isDebug") else false,
52
81
  logLevel = if (config.hasKey("logLevel")) config.getInt("logLevel") else if (config.hasKey("isDebug") && config.getBoolean("isDebug")) 0 else 2,
53
82
  customerUserId = if (config.hasKey("customerUserId")) config.getString("customerUserId") else null,
83
+ autoTrackSessions = if (config.hasKey("autoTrackSessions")) config.getBoolean("autoTrackSessions") else true,
84
+ autoRefreshAttribution = if (config.hasKey("autoRefreshAttribution")) config.getBoolean("autoRefreshAttribution") else true,
85
+ googleAdsConsent = googleAdsConsentFrom(config),
54
86
  )
55
87
  sdk().configure(sdkConfig)
56
- promise.resolve(null)
88
+ promise.resolve(true)
57
89
  }
58
90
  }
59
91
 
@@ -63,10 +95,10 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
63
95
  val type = AppSprintEventType.entries.find { it.wireValue == eventType } ?: AppSprintEventType.CUSTOM
64
96
  val params = mutableMapOf<String, Any?>()
65
97
  parameters?.toHashMap()?.forEach { (key, value) -> params[key] = value }
66
- if (revenue != null && revenue != 0.0) params["revenue"] = revenue
98
+ if (revenue != null) params["revenue"] = revenue
67
99
  if (currency != null) params["currency"] = currency
68
100
  sdk().sendEvent(type, name, if (params.isNotEmpty()) params else null)
69
- promise.resolve(null)
101
+ promise.resolve(true)
70
102
  }
71
103
  }
72
104
 
@@ -105,42 +137,101 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
105
137
  }
106
138
  }
107
139
 
140
+ @ReactMethod
141
+ fun refreshAttribution(promise: Promise) {
142
+ runAsync("REFRESH_ATTRIBUTION_ERROR", promise) {
143
+ promise.resolve(sdk().refreshAttribution()?.let { attributionToMap(it) })
144
+ }
145
+ }
146
+
108
147
  @ReactMethod
109
148
  fun enableAppleAdsAttribution(promise: Promise) {
110
- sdk().enableAppleAdsAttribution()
111
- promise.resolve(null)
149
+ runAsync("APPLE_ADS_ERROR", promise) {
150
+ promise.resolve(sdk().enableAppleAdsAttribution())
151
+ }
112
152
  }
113
153
 
114
154
  @ReactMethod
115
155
  fun getAppSprintId(promise: Promise) {
116
- promise.resolve(sdk().getAppSprintId())
156
+ resolveSync("GET_APPSPRINT_ID_ERROR", promise) { sdk().getAppSprintId() }
117
157
  }
118
158
 
119
159
  @ReactMethod
120
160
  fun getAttribution(promise: Promise) {
121
- val attr = sdk().getAttribution()
122
- if (attr == null) {
123
- promise.resolve(null)
124
- return
161
+ resolveSync("GET_ATTRIBUTION_ERROR", promise) {
162
+ sdk().getAttribution()?.let { attributionToMap(it) }
125
163
  }
164
+ }
165
+
166
+ @ReactMethod
167
+ fun getAttributionParams(promise: Promise) {
168
+ resolveSync("GET_ATTRIBUTION_PARAMS_ERROR", promise) {
169
+ val map = Arguments.createMap()
170
+ sdk().getAttributionParams().forEach { (key, value) -> map.putString(key, value) }
171
+ map
172
+ }
173
+ }
174
+
175
+ private fun attributionToMap(attr: AttributionResult): WritableMap {
126
176
  val map = Arguments.createMap()
177
+ map.putBoolean("isAttributed", attr.isAttributed)
127
178
  map.putString("source", attr.source)
128
179
  map.putDouble("confidence", attr.confidence)
180
+ attr.matchType?.let { map.putString("matchType", it) }
129
181
  attr.campaignName?.let { map.putString("campaignName", it) }
182
+ attr.link?.let {
183
+ val link = Arguments.createMap()
184
+ link.putString("id", it.id)
185
+ link.putString("name", it.name)
186
+ map.putMap("link", link)
187
+ }
188
+ attr.appleAds?.let {
189
+ val appleAds = Arguments.createMap()
190
+ appleAds.putString("campaignId", it.campaignId)
191
+ it.orgId?.let { value -> appleAds.putString("orgId", value) }
192
+ it.adGroupId?.let { value -> appleAds.putString("adGroupId", value) }
193
+ it.keywordId?.let { value -> appleAds.putString("keywordId", value) }
194
+ it.adId?.let { value -> appleAds.putString("adId", value) }
195
+ it.countryOrRegion?.let { value -> appleAds.putString("countryOrRegion", value) }
196
+ it.claimType?.let { value -> appleAds.putString("claimType", value) }
197
+ it.clickDate?.let { value -> appleAds.putString("clickDate", value) }
198
+ it.impressionDate?.let { value -> appleAds.putString("impressionDate", value) }
199
+ it.conversionType?.let { value -> appleAds.putString("conversionType", value) }
200
+ it.supplyPlacement?.let { value -> appleAds.putString("supplyPlacement", value) }
201
+ map.putMap("appleAds", appleAds)
202
+ }
130
203
  attr.utmSource?.let { map.putString("utmSource", it) }
131
204
  attr.utmMedium?.let { map.putString("utmMedium", it) }
132
205
  attr.utmCampaign?.let { map.putString("utmCampaign", it) }
133
- promise.resolve(map)
206
+ attr.utmContent?.let { map.putString("utmContent", it) }
207
+ attr.utmTerm?.let { map.putString("utmTerm", it) }
208
+ return map
209
+ }
210
+
211
+ private fun googleAdsConsentFrom(config: ReadableMap): GoogleAdsConsent? {
212
+ if (!config.hasKey("googleAdsConsent") || config.isNull("googleAdsConsent")) return null
213
+ val consent = config.getMap("googleAdsConsent") ?: return null
214
+ if (!consent.hasKey("adUserData") || consent.isNull("adUserData")) return null
215
+ if (consent.getType("adUserData") != ReadableType.String) return null
216
+ return googleAdsConsentStatus(consent.getString("adUserData"))
217
+ ?.let { GoogleAdsConsent(it) }
218
+ }
219
+
220
+ private fun googleAdsConsentStatus(value: String?): GoogleAdsConsentStatus? {
221
+ val normalized = value?.trim()?.uppercase(java.util.Locale.US) ?: return null
222
+ return GoogleAdsConsentStatus.entries.firstOrNull {
223
+ it.wireValue == normalized || it.name == normalized
224
+ }
134
225
  }
135
226
 
136
227
  @ReactMethod
137
228
  fun isInitialized(promise: Promise) {
138
- promise.resolve(sdk().isInitialized())
229
+ resolveSync("IS_INITIALIZED_ERROR", promise) { sdk().isInitialized() }
139
230
  }
140
231
 
141
232
  @ReactMethod
142
233
  fun isSdkDisabled(promise: Promise) {
143
- promise.resolve(sdk().isSdkDisabled())
234
+ resolveSync("SDK_DISABLED_ERROR", promise) { sdk().isSdkDisabled() }
144
235
  }
145
236
 
146
237
  @ReactMethod
@@ -155,18 +246,18 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
155
246
 
156
247
  @ReactMethod
157
248
  fun getDeviceInfo(promise: Promise) {
158
- try {
249
+ runAsync("DEVICE_INFO_ERROR", promise) {
250
+ val deviceInfo = AppSprintNative(reactApplicationContext).getDeviceInfo(includeAdvertisingId = true)
159
251
  val info = Arguments.createMap()
160
- info.putString("deviceModel", android.os.Build.MODEL)
161
- val metrics = reactApplicationContext.resources.displayMetrics
162
- info.putInt("screenWidth", metrics.widthPixels)
163
- info.putInt("screenHeight", metrics.heightPixels)
164
- info.putString("locale", java.util.Locale.getDefault().toLanguageTag())
165
- info.putString("timezone", java.util.TimeZone.getDefault().id)
166
- info.putString("osVersion", android.os.Build.VERSION.RELEASE)
252
+ deviceInfo.deviceModel?.let { info.putString("deviceModel", it) }
253
+ deviceInfo.screenWidth?.let { info.putInt("screenWidth", it) }
254
+ deviceInfo.screenHeight?.let { info.putInt("screenHeight", it) }
255
+ deviceInfo.locale?.let { info.putString("locale", it) }
256
+ deviceInfo.timezone?.let { info.putString("timezone", it) }
257
+ deviceInfo.osVersion?.let { info.putString("osVersion", it) }
258
+ deviceInfo.appVersion?.let { info.putString("appVersion", it) }
259
+ deviceInfo.gaid?.let { info.putString("gaid", it) }
167
260
  promise.resolve(info)
168
- } catch (e: Exception) {
169
- promise.reject("DEVICE_INFO_ERROR", e.message, e)
170
261
  }
171
262
  }
172
263
 
@@ -177,6 +268,11 @@ class AppSprintBridgeModule(reactContext: ReactApplicationContext) : ReactContex
177
268
 
178
269
  @ReactMethod
179
270
  fun requestTrackingAuthorization(promise: Promise) {
180
- promise.resolve(false) // iOS only
271
+ promise.resolve(AppSprintNative(reactApplicationContext).requestTrackingAuthorization())
272
+ }
273
+
274
+ override fun invalidate() {
275
+ bridgeExecutor.shutdown()
276
+ super.invalidate()
181
277
  }
182
278
  }
@@ -1,6 +1,11 @@
1
1
  require "json"
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+ repository = package["repository"]
5
+ repository_url = repository.is_a?(Hash) ? repository["url"] : repository
6
+ repository_url = repository_url&.sub(/^git\+/, "")
7
+
8
+ raise "package.json repository.url must be set" if repository_url.nil? || repository_url.empty?
4
9
 
5
10
  Pod::Spec.new do |s|
6
11
  s.name = "appsprint-react-native"
@@ -11,7 +16,7 @@ Pod::Spec.new do |s|
11
16
  s.authors = package["author"]
12
17
 
13
18
  s.platforms = { :ios => "14.0" }
14
- s.source = { :git => package["repository"], :tag => "#{s.version}" }
19
+ s.source = { :git => repository_url, :tag => "v#{s.version}" }
15
20
 
16
21
  s.source_files = "ios/AppSprintBridge.{swift,m}"
17
22
  s.swift_version = "5.0"
@@ -28,6 +28,9 @@ RCT_EXTERN_METHOD(setCustomerUserId:(NSString *)userId
28
28
  resolve:(RCTPromiseResolveBlock)resolve
29
29
  rejecter:(RCTPromiseRejectBlock)reject)
30
30
 
31
+ RCT_EXTERN_METHOD(refreshAttribution:(RCTPromiseResolveBlock)resolve
32
+ rejecter:(RCTPromiseRejectBlock)reject)
33
+
31
34
  RCT_EXTERN_METHOD(enableAppleAdsAttribution:(RCTPromiseResolveBlock)resolve
32
35
  rejecter:(RCTPromiseRejectBlock)reject)
33
36
 
@@ -37,6 +40,9 @@ RCT_EXTERN_METHOD(getAppSprintId:(RCTPromiseResolveBlock)resolve
37
40
  RCT_EXTERN_METHOD(getAttribution:(RCTPromiseResolveBlock)resolve
38
41
  rejecter:(RCTPromiseRejectBlock)reject)
39
42
 
43
+ RCT_EXTERN_METHOD(getAttributionParams:(RCTPromiseResolveBlock)resolve
44
+ rejecter:(RCTPromiseRejectBlock)reject)
45
+
40
46
  RCT_EXTERN_METHOD(isInitialized:(RCTPromiseResolveBlock)resolve
41
47
  rejecter:(RCTPromiseRejectBlock)reject)
42
48