@dl2app/react-native 0.1.0 → 0.2.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/DL2ReactNative.podspec +26 -0
- package/README.md +64 -36
- package/android/build.gradle +30 -9
- package/android/src/main/java/app/dl2/reactnative/DL2Module.kt +109 -55
- package/android/src/main/java/app/dl2/reactnative/DL2Package.kt +17 -0
- package/build/plugin/src/withDL2.d.ts +1 -1
- package/build/plugin/src/withDL2.js +13 -8
- package/build/src/PasteButton.js +2 -2
- package/build/src/core.d.ts +1 -1
- package/build/src/core.js +121 -17
- package/build/src/index.js +2 -2
- package/build/src/types.d.ts +11 -1
- package/ios/DL2Module.m +22 -0
- package/ios/DL2Module.swift +48 -26
- package/ios/DL2PasteButtonView.swift +25 -8
- package/ios/PrivacyInfo.xcprivacy +9 -0
- package/package.json +5 -3
- package/plugin/src/withDL2.ts +14 -6
- package/src/PasteButton.ts +3 -3
- package/src/core.ts +112 -17
- package/src/index.ts +3 -3
- package/src/types.ts +11 -1
- package/expo-module.config.json +0 -5
- package/ios/DL2.podspec +0 -22
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'DL2ReactNative'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = 'DL2'
|
|
12
|
+
s.homepage = 'https://dl2.app'
|
|
13
|
+
s.platforms = { :ios => '13.4' }
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { :git => 'https://github.com/dl2app/dl2-react-native.git', :tag => "v#{s.version}" }
|
|
16
|
+
|
|
17
|
+
s.source_files = 'ios/*.{h,m,swift}'
|
|
18
|
+
s.resource_bundles = { 'DL2ReactNative_Privacy' => ['ios/PrivacyInfo.xcprivacy'] }
|
|
19
|
+
|
|
20
|
+
# A plain React Native module: no Expo dependency. React Native 0.71+ provides the helper.
|
|
21
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
22
|
+
install_modules_dependencies(s)
|
|
23
|
+
else
|
|
24
|
+
s.dependency 'React-Core'
|
|
25
|
+
end
|
|
26
|
+
end
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dl2app/react-native
|
|
2
2
|
|
|
3
|
-
DL2 deep links for React Native
|
|
3
|
+
DL2 deep links for React Native, with or without Expo.
|
|
4
4
|
|
|
5
5
|
- Opens the right screen when a DL2 link opens your app (iOS Universal Links, Android App Links, optional custom scheme).
|
|
6
6
|
- Deferred deep links: a user who installs from a link lands on that link's screen on first launch.
|
|
@@ -11,11 +11,61 @@ DL2 deep links for React Native and Expo.
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
- React Native 0.74 or newer, old or new architecture.
|
|
14
|
-
- Expo SDK 51 or newer
|
|
15
|
-
-
|
|
14
|
+
- Works in a plain React Native app and in Expo (SDK 51 or newer). The native code is a plain React Native module: it does not need Expo or Expo Modules.
|
|
15
|
+
- Version 0.2.0 was compiled for Android and iOS in a fresh React Native 0.87 app and a fresh Expo SDK 57 app, and run on an Android phone in the React Native app.
|
|
16
|
+
- Expo: a development build. **Expo Go does not work**, because the SDK has native code and your app needs its own associated domains.
|
|
16
17
|
|
|
17
18
|
## Install
|
|
18
19
|
|
|
20
|
+
### React Native (no Expo)
|
|
21
|
+
|
|
22
|
+
1. Install the package and the iOS pod. Autolinking registers the native module on both platforms; there is nothing to add to `MainApplication` or the `Podfile`.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @dl2app/react-native
|
|
26
|
+
cd ios && pod install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. iOS: in Xcode, open your target, **Signing & Capabilities**, add **Associated Domains**, and add one line per host:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
applinks:myapp.dl2.app
|
|
33
|
+
applinks:myapp-alt.dl2.app
|
|
34
|
+
applinks:go.myapp.com
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. iOS: pass incoming links to React Native. The React Native template does not do this for you. In `AppDelegate.swift`:
|
|
38
|
+
|
|
39
|
+
```swift
|
|
40
|
+
import React
|
|
41
|
+
|
|
42
|
+
// Inside your AppDelegate class. Write "override func" if it subclasses RCTAppDelegate.
|
|
43
|
+
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
44
|
+
return RCTLinkingManager.application(app, open: url, options: options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
|
48
|
+
return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. Android: in `android/app/src/main/AndroidManifest.xml`, inside the main `<activity>`:
|
|
53
|
+
|
|
54
|
+
```xml
|
|
55
|
+
<intent-filter android:autoVerify="true">
|
|
56
|
+
<action android:name="android.intent.action.VIEW" />
|
|
57
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
58
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
59
|
+
<data android:scheme="https" android:host="myapp.dl2.app" />
|
|
60
|
+
<data android:scheme="https" android:host="myapp-alt.dl2.app" />
|
|
61
|
+
<data android:scheme="https" android:host="go.myapp.com" />
|
|
62
|
+
</intent-filter>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The main activity must keep `android:launchMode="singleTask"` (the template's default), so a link opened while the app runs reaches the running app.
|
|
66
|
+
|
|
67
|
+
5. Rebuild the app (`npx react-native run-ios` / `run-android`). A Metro reload is not enough after adding native code.
|
|
68
|
+
|
|
19
69
|
### Expo
|
|
20
70
|
|
|
21
71
|
```sh
|
|
@@ -42,44 +92,17 @@ Plugin options:
|
|
|
42
92
|
| --- | --- | --- |
|
|
43
93
|
| `subdomain` | none | Your DL2 project subdomain. Adds `myapp.dl2.app` and `myapp-alt.dl2.app`. |
|
|
44
94
|
| `domains` | `[]` | Custom domains. Without `subdomain`, list every host here, including the `-alt` one. |
|
|
45
|
-
| `iosDeveloperMode` | `false` | Adds `?mode=developer` to the iOS entries. Use it for development builds only. |
|
|
95
|
+
| `iosDeveloperMode` | `false` | Adds `?mode=developer` to the iOS entries the plugin adds (entries you already list are left as they are). Use it for development builds only. |
|
|
46
96
|
| `androidAutoVerify` | `true` | Adds `android:autoVerify="true"` to the intent filter. |
|
|
47
97
|
| `uriScheme` | none | Also registers a custom scheme such as `myapp`. |
|
|
48
98
|
| `rootDomain` | `dl2.app` | Only for self-hosted DL2. |
|
|
49
99
|
|
|
50
100
|
You need `subdomain` or at least one entry in `domains`. The plugin stops the build with a message that says what to change when an option is wrong, for example a host written with `https://`.
|
|
51
101
|
|
|
52
|
-
The plugin adds to `ios.associatedDomains`, `android.intentFilters` and `scheme`. It keeps the entries you already have and is safe to run any number of times. The Play Install Referrer library comes with the SDK's own Gradle file, so there is nothing to add for it.
|
|
102
|
+
The plugin adds to `ios.associatedDomains`, `android.intentFilters` and `scheme`. It keeps the entries you already have and is safe to run any number of times. An existing https intent filter only counts as covering a host when it is not limited to a path, port or type. The Play Install Referrer library comes with the SDK's own Gradle file, so there is nothing to add for it.
|
|
53
103
|
|
|
54
104
|
Always include the `-alt` host. Pages on your main link host use it for their "Open in app" button: iOS only opens an app from a tap that crosses to a different domain.
|
|
55
105
|
|
|
56
|
-
### Bare React Native
|
|
57
|
-
|
|
58
|
-
1. Install Expo Modules if your app does not have them: `npx install-expo-modules@latest`.
|
|
59
|
-
2. `npm install @dl2app/react-native`, then `cd ios && pod install`.
|
|
60
|
-
3. iOS: in Xcode, open your target, **Signing & Capabilities**, add **Associated Domains**, and add one line per host:
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
applinks:myapp.dl2.app
|
|
64
|
-
applinks:myapp-alt.dl2.app
|
|
65
|
-
applinks:go.myapp.com
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
In `AppDelegate`, make sure React Native's `RCTLinkingManager` receives `application:continueUserActivity:restorationHandler:` and `application:openURL:options:` (the React Native template explains both under "Linking").
|
|
69
|
-
|
|
70
|
-
4. Android: in `android/app/src/main/AndroidManifest.xml`, inside the main `<activity>`:
|
|
71
|
-
|
|
72
|
-
```xml
|
|
73
|
-
<intent-filter android:autoVerify="true">
|
|
74
|
-
<action android:name="android.intent.action.VIEW" />
|
|
75
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
76
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
77
|
-
<data android:scheme="https" android:host="myapp.dl2.app" />
|
|
78
|
-
<data android:scheme="https" android:host="myapp-alt.dl2.app" />
|
|
79
|
-
<data android:scheme="https" android:host="go.myapp.com" />
|
|
80
|
-
</intent-filter>
|
|
81
|
-
```
|
|
82
|
-
|
|
83
106
|
The dashboard (Project, Setup) shows these snippets filled in with your hosts.
|
|
84
107
|
|
|
85
108
|
## Use
|
|
@@ -140,6 +163,8 @@ type DL2LinkEvent = {
|
|
|
140
163
|
- Links that arrive before any listener is registered are kept (up to 20) and delivered to the first listener.
|
|
141
164
|
- The same URL within 2 seconds is delivered once.
|
|
142
165
|
- If the DL2 API cannot be reached within 3 seconds, the event is built from the URL itself (`path` and `params` from the URL, empty `data`), so your app still navigates.
|
|
166
|
+
- A link in your own `uriScheme` is always built from the URL, without a request: DL2 only stores https links.
|
|
167
|
+
- Android: a link is not delivered again when the app is reopened from Recents after its process was stopped.
|
|
143
168
|
|
|
144
169
|
### `DL2.getInitialLink()` returns `Promise<DL2LinkEvent | null>`
|
|
145
170
|
|
|
@@ -156,7 +181,8 @@ Records an event, for example `DL2.track("purchase", { value: 19.99, currency: "
|
|
|
156
181
|
- Returns at once. Events are sent in order in the background.
|
|
157
182
|
- Offline, events wait in a queue that survives app restarts. The queue holds 100 events; beyond that the oldest are dropped.
|
|
158
183
|
- Failed deliveries are retried after 2, 4, 8 seconds and so on, up to 5 minutes apart. `DL2.flush()` tries immediately.
|
|
159
|
-
- Delivery is at least once: after a timeout an event can
|
|
184
|
+
- Delivery is at least once: after a timeout an event can be sent twice. Each event carries an `event_id` made when it is queued, the same on every retry, so the server can count it once. Events are stamped with the time the server receives them.
|
|
185
|
+
- An event the server rejects (a 4xx answer other than 429) is dropped and reported through `onError`.
|
|
160
186
|
|
|
161
187
|
### `DL2.createLink({ path, data?, og?, utm? })` returns `Promise<{ url } | null>`
|
|
162
188
|
|
|
@@ -205,7 +231,9 @@ DL2.configure({ ..., deferred: { useClipboard: true } });
|
|
|
205
231
|
1. `configure` loads or creates the install ID (stored in `UserDefaults` on iOS, `SharedPreferences` on Android).
|
|
206
232
|
2. If the app was opened by a DL2 link, that link is used. On a first launch it is reported with `POST /v1/sdk/open` (`launch_url` set); later with `POST /v1/sdk/resolve`.
|
|
207
233
|
3. Otherwise, on first launch only, the SDK asks `POST /v1/sdk/open` whether this install came from a link. Android sends the Play install referrer, which is exact. iOS is matched on network and device signals; DL2 returns nothing rather than risk giving a user someone else's link.
|
|
208
|
-
4. If the device is offline on first launch, the check is tried again on the next launch, three launches at most.
|
|
234
|
+
4. If the device is offline on first launch, or the API answers 429 or 5xx, the check is tried again on the next launch, three launches at most.
|
|
235
|
+
5. When the app was installed more than 24 hours ago (for example when the SDK arrives in an app update), the first-launch call carries `skip_fingerprint: true`: the install is recorded and exact matches (Play install referrer, clipboard) still work, but it is not matched on network and device signals.
|
|
236
|
+
6. Android: when a backup or device transfer restores the SDK's storage into a new install, the restored install ID, first-launch flag and queued events are discarded and the launch counts as a first launch.
|
|
209
237
|
|
|
210
238
|
## Troubleshooting
|
|
211
239
|
|
|
@@ -244,7 +272,7 @@ On iOS development builds, set `iosDeveloperMode: true` and turn on Settings, De
|
|
|
244
272
|
**Nothing happens and there is no error**
|
|
245
273
|
|
|
246
274
|
- Turn on `debug: true` and read the `[DL2]` lines.
|
|
247
|
-
- "DL2 native module not found": the app was not rebuilt after installing the package, or it is running in Expo Go.
|
|
275
|
+
- "DL2 native module not found": the app was not rebuilt after installing the package (on iOS, run `pod install` first), or it is running in Expo Go.
|
|
248
276
|
- The host of the URL is not in `linkDomains`. The SDK ignores URLs on other hosts.
|
|
249
277
|
|
|
250
278
|
**Deferred links do not match on iOS**
|
|
@@ -266,7 +294,7 @@ What the SDK sends to DL2:
|
|
|
266
294
|
|
|
267
295
|
The SDK does not read the IDFA or the Android advertising ID, does not use any other device identifier, does not track users across apps or sites, and DL2 does not sell or share this data. You do not need the App Tracking Transparency prompt for DL2.
|
|
268
296
|
|
|
269
|
-
The package includes `ios/PrivacyInfo.xcprivacy` (no tracking, no tracking domains, `UserDefaults` reason `CA92.1`). Xcode merges it into your app's privacy report.
|
|
297
|
+
The package includes `ios/PrivacyInfo.xcprivacy` (no tracking, no tracking domains, `UserDefaults` reason `CA92.1`, file timestamp reason `C617.1`: the creation date of the app's own Library directory, read as the install time). Xcode merges it into your app's privacy report.
|
|
270
298
|
|
|
271
299
|
### App Store: App Privacy answers
|
|
272
300
|
|
package/android/build.gradle
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
// Only used when the app's own build does not already provide the Kotlin plugin.
|
|
3
|
+
ext.dl2KotlinVersion = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '1.9.24'
|
|
4
|
+
repositories {
|
|
5
|
+
google()
|
|
6
|
+
mavenCentral()
|
|
7
|
+
}
|
|
8
|
+
dependencies {
|
|
9
|
+
if (project == rootProject) {
|
|
10
|
+
classpath 'com.android.tools.build:gradle:8.5.0'
|
|
11
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$dl2KotlinVersion"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
apply plugin: 'com.android.library'
|
|
17
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
2
18
|
|
|
3
19
|
group = 'app.dl2.reactnative'
|
|
4
|
-
version = '0.
|
|
20
|
+
version = '0.2.0'
|
|
5
21
|
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
useCoreDependencies()
|
|
10
|
-
useDefaultAndroidSdkVersions()
|
|
11
|
-
useExpoPublishing()
|
|
22
|
+
def dl2Ext(name, fallback) {
|
|
23
|
+
rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
android {
|
|
14
27
|
namespace "app.dl2.reactnative"
|
|
28
|
+
compileSdkVersion dl2Ext('compileSdkVersion', 35)
|
|
15
29
|
defaultConfig {
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
minSdkVersion dl2Ext('minSdkVersion', 24)
|
|
31
|
+
targetSdkVersion dl2Ext('targetSdkVersion', 35)
|
|
18
32
|
}
|
|
19
33
|
}
|
|
20
34
|
|
|
35
|
+
repositories {
|
|
36
|
+
google()
|
|
37
|
+
mavenCentral()
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
dependencies {
|
|
41
|
+
// A plain React Native module: no Expo dependency. The app's React Native version is used.
|
|
42
|
+
implementation 'com.facebook.react:react-android'
|
|
22
43
|
// Play Install Referrer: carries the click ID through a Play Store install.
|
|
23
44
|
implementation 'com.android.installreferrer:installreferrer:2.2'
|
|
24
45
|
}
|
|
@@ -1,79 +1,127 @@
|
|
|
1
1
|
package app.dl2.reactnative
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
4
5
|
import android.content.SharedPreferences
|
|
5
6
|
import android.os.Build
|
|
6
7
|
import com.android.installreferrer.api.InstallReferrerClient
|
|
7
8
|
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import com.facebook.react.bridge.Arguments
|
|
10
|
+
import com.facebook.react.bridge.Promise
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
13
|
+
import com.facebook.react.bridge.ReactMethod
|
|
12
14
|
import java.util.Locale
|
|
13
15
|
import java.util.TimeZone
|
|
14
16
|
import java.util.UUID
|
|
15
17
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
16
18
|
|
|
17
|
-
// Key-value storage, device info
|
|
18
|
-
|
|
19
|
+
// Key-value storage, device info, the install time, the launch intent's history flag and the
|
|
20
|
+
// Play Install Referrer. Nothing else.
|
|
21
|
+
class DL2Module(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
19
22
|
private val context: Context
|
|
20
|
-
get() =
|
|
23
|
+
get() = reactApplicationContext
|
|
21
24
|
|
|
22
25
|
private val prefs: SharedPreferences
|
|
23
26
|
get() = context.getSharedPreferences("app.dl2.sdk", Context.MODE_PRIVATE)
|
|
24
27
|
|
|
25
|
-
override fun
|
|
26
|
-
Name("DL2")
|
|
28
|
+
override fun getName() = "DL2"
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
30
|
+
@ReactMethod
|
|
31
|
+
fun getItem(key: String, promise: Promise) {
|
|
32
|
+
settle(promise) { prefs.getString(key, null) }
|
|
33
|
+
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
@ReactMethod
|
|
36
|
+
fun setItem(key: String, value: String, promise: Promise) {
|
|
37
|
+
settle(promise) {
|
|
33
38
|
prefs.edit().putString(key, value).apply()
|
|
39
|
+
null
|
|
34
40
|
}
|
|
41
|
+
}
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
@ReactMethod
|
|
44
|
+
fun removeItem(key: String, promise: Promise) {
|
|
45
|
+
settle(promise) {
|
|
37
46
|
prefs.edit().remove(key).apply()
|
|
47
|
+
null
|
|
38
48
|
}
|
|
49
|
+
}
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
51
|
+
@ReactMethod
|
|
52
|
+
fun uuid(promise: Promise) {
|
|
53
|
+
settle(promise) { UUID.randomUUID().toString() }
|
|
54
|
+
}
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
@ReactMethod
|
|
57
|
+
fun getDeviceInfo(promise: Promise) {
|
|
58
|
+
settle(promise) {
|
|
45
59
|
val ctx = context
|
|
46
60
|
val info = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
|
47
|
-
|
|
48
|
-
"osVersion"
|
|
49
|
-
"appId"
|
|
50
|
-
"appVersion"
|
|
51
|
-
"lang"
|
|
52
|
-
"tz"
|
|
53
|
-
|
|
61
|
+
Arguments.createMap().apply {
|
|
62
|
+
putString("osVersion", Build.VERSION.RELEASE ?: "")
|
|
63
|
+
putString("appId", ctx.packageName)
|
|
64
|
+
putString("appVersion", info.versionName ?: "")
|
|
65
|
+
putString("lang", Locale.getDefault().toLanguageTag())
|
|
66
|
+
putString("tz", TimeZone.getDefault().id)
|
|
67
|
+
}
|
|
54
68
|
}
|
|
69
|
+
}
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
// When the app was installed, in ms since epoch. An update keeps it; a reinstall or a
|
|
72
|
+
// restore onto another device does not.
|
|
73
|
+
@ReactMethod
|
|
74
|
+
fun getInstalledAt(promise: Promise) {
|
|
75
|
+
settle(promise) {
|
|
76
|
+
val ctx = context
|
|
77
|
+
ctx.packageManager.getPackageInfo(ctx.packageName, 0).firstInstallTime.toDouble()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// True when the activity was reopened from Recents: its intent is the one from the original
|
|
82
|
+
// launch, so the URL in it was already delivered.
|
|
83
|
+
@ReactMethod
|
|
84
|
+
fun isLaunchedFromHistory(promise: Promise) {
|
|
85
|
+
settle(promise) {
|
|
86
|
+
val flags = reactApplicationContext.currentActivity?.intent?.flags ?: 0
|
|
87
|
+
(flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Resolves with the referrer string, or null when Play has none. Read once, then cached.
|
|
92
|
+
@ReactMethod
|
|
93
|
+
fun getInstallReferrer(promise: Promise) {
|
|
94
|
+
val cached = prefs.getString(REFERRER_KEY, null)
|
|
95
|
+
if (cached != null) {
|
|
96
|
+
promise.resolve(cached)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
val settled = AtomicBoolean(false)
|
|
100
|
+
val finish = { value: String? ->
|
|
101
|
+
if (settled.compareAndSet(false, true)) {
|
|
102
|
+
promise.resolve(value)
|
|
68
103
|
}
|
|
104
|
+
}
|
|
105
|
+
var client: InstallReferrerClient? = null
|
|
106
|
+
val end = {
|
|
69
107
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
client?.endConnection()
|
|
109
|
+
} catch (e: Exception) {
|
|
110
|
+
// already closed
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
val c = InstallReferrerClient.newBuilder(context).build()
|
|
115
|
+
client = c
|
|
116
|
+
c.startConnection(object : InstallReferrerStateListener {
|
|
117
|
+
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
|
118
|
+
// Called on the main thread, and reading the referrer is a call into the Play
|
|
119
|
+
// Store's process: do it on a thread of its own.
|
|
120
|
+
Thread {
|
|
73
121
|
var referrer: String? = null
|
|
74
122
|
try {
|
|
75
123
|
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
76
|
-
referrer =
|
|
124
|
+
referrer = c.installReferrer?.installReferrer
|
|
77
125
|
if (referrer != null) {
|
|
78
126
|
prefs.edit().putString(REFERRER_KEY, referrer).apply()
|
|
79
127
|
}
|
|
@@ -81,23 +129,29 @@ class DL2Module : Module() {
|
|
|
81
129
|
} catch (e: Exception) {
|
|
82
130
|
referrer = null
|
|
83
131
|
} finally {
|
|
84
|
-
|
|
85
|
-
client.endConnection()
|
|
86
|
-
} catch (e: Exception) {
|
|
87
|
-
// already closed
|
|
88
|
-
}
|
|
132
|
+
end()
|
|
89
133
|
}
|
|
90
134
|
finish(referrer)
|
|
91
|
-
}
|
|
135
|
+
}.start()
|
|
136
|
+
}
|
|
92
137
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
override fun onInstallReferrerServiceDisconnected() {
|
|
139
|
+
end()
|
|
140
|
+
finish(null)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
} catch (e: Exception) {
|
|
144
|
+
// No Play Store on this device (emulator image, Huawei, sideloaded build).
|
|
145
|
+
end()
|
|
146
|
+
finish(null)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private fun settle(promise: Promise, block: () -> Any?) {
|
|
151
|
+
try {
|
|
152
|
+
promise.resolve(block())
|
|
153
|
+
} catch (e: Exception) {
|
|
154
|
+
promise.reject("dl2_native", e.message, e)
|
|
101
155
|
}
|
|
102
156
|
}
|
|
103
157
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package app.dl2.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
|
+
// Found by React Native's autolinking; nothing to register by hand.
|
|
9
|
+
class DL2Package : ReactPackage {
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return listOf(DL2Module(reactContext))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return emptyList()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -51,7 +51,7 @@ export type ExpoConfigLike = {
|
|
|
51
51
|
};
|
|
52
52
|
/** Validates the plugin options and returns the full host list. Throws with a fix in the message. */
|
|
53
53
|
export declare function normalizeOptions(raw: unknown): NormalizedOptions;
|
|
54
|
-
/** Adds `applinks:` entries, keeping every entry the app already has. */
|
|
54
|
+
/** Adds `applinks:` entries, keeping every entry the app already has exactly as it is. */
|
|
55
55
|
export declare function mergeAssociatedDomains(existing: unknown, domains: string[], developerMode: boolean): string[];
|
|
56
56
|
/** Adds one https intent filter for the hosts that are not covered yet, and one for the scheme. */
|
|
57
57
|
export declare function mergeIntentFilters(existing: unknown, domains: string[], autoVerify: boolean, uriScheme: string | null): IntentFilter[];
|
|
@@ -94,16 +94,14 @@ function normalizeOptions(raw) {
|
|
|
94
94
|
uriScheme,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
/** Adds `applinks:` entries, keeping every entry the app already has. */
|
|
97
|
+
/** Adds `applinks:` entries, keeping every entry the app already has exactly as it is. */
|
|
98
98
|
function mergeAssociatedDomains(existing, domains, developerMode) {
|
|
99
99
|
const out = Array.isArray(existing) ? existing.filter((e) => typeof e === "string") : [];
|
|
100
100
|
for (const d of domains) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
out[i] = wanted; // same host already listed: only the mode can differ
|
|
101
|
+
// A host the app already lists keeps its own mode: the option only applies to entries added here.
|
|
102
|
+
if (out.some((e) => e.split("?")[0].toLowerCase() === `applinks:${d}`))
|
|
103
|
+
continue;
|
|
104
|
+
out.push(`applinks:${d}${developerMode ? "?mode=developer" : ""}`);
|
|
107
105
|
}
|
|
108
106
|
return out;
|
|
109
107
|
}
|
|
@@ -113,6 +111,11 @@ function list(v) {
|
|
|
113
111
|
function isBrowsableView(f) {
|
|
114
112
|
return f.action === "VIEW" && list(f.category).indexOf("BROWSABLE") >= 0;
|
|
115
113
|
}
|
|
114
|
+
const RESTRICTIONS = ["port", "path", "pathPrefix", "pathPattern", "pathSuffix", "pathAdvancedPattern", "mimeType"];
|
|
115
|
+
/** Android applies every path, port and type in a filter to all of its hosts, so one of them narrows the whole filter. */
|
|
116
|
+
function isRestricted(f) {
|
|
117
|
+
return list(f.data).some((d) => RESTRICTIONS.some((k) => d[k] !== undefined && d[k] !== null && d[k] !== ""));
|
|
118
|
+
}
|
|
116
119
|
/** Adds one https intent filter for the hosts that are not covered yet, and one for the scheme. */
|
|
117
120
|
function mergeIntentFilters(existing, domains, autoVerify, uriScheme) {
|
|
118
121
|
const out = Array.isArray(existing) ? existing.slice() : [];
|
|
@@ -121,8 +124,10 @@ function mergeIntentFilters(existing, domains, autoVerify, uriScheme) {
|
|
|
121
124
|
for (const f of out) {
|
|
122
125
|
if (!f || !isBrowsableView(f))
|
|
123
126
|
continue;
|
|
127
|
+
// A filter limited to some paths does not open every link of the host.
|
|
128
|
+
const open = !isRestricted(f);
|
|
124
129
|
for (const d of list(f.data)) {
|
|
125
|
-
if (d.scheme === "https" && typeof d.host === "string" && (!autoVerify || f.autoVerify === true))
|
|
130
|
+
if (open && d.scheme === "https" && typeof d.host === "string" && (!autoVerify || f.autoVerify === true))
|
|
126
131
|
covered.add(d.host.toLowerCase());
|
|
127
132
|
if (d.scheme && !d.host)
|
|
128
133
|
schemes.add(d.scheme);
|
package/build/src/PasteButton.js
CHANGED
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DL2PasteButton = DL2PasteButton;
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
|
-
const expo_modules_core_1 = require("expo-modules-core");
|
|
7
6
|
let NativeView;
|
|
8
7
|
function nativeView() {
|
|
9
8
|
if (NativeView === undefined) {
|
|
10
9
|
try {
|
|
11
|
-
|
|
10
|
+
// Asking for a view the app was not built with fails only when it renders, so check first.
|
|
11
|
+
NativeView = react_native_1.UIManager.hasViewManagerConfig("DL2PasteButton") ? (0, react_native_1.requireNativeComponent)("DL2PasteButton") : null;
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
NativeView = null;
|
package/build/src/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DL2Config, DL2CreateLinkInput, DL2DebugInfo, DL2Deps, DL2LinkEvent, DL2TrackOptions } from "./types";
|
|
2
|
-
export declare const SDK_VERSION = "0.
|
|
2
|
+
export declare const SDK_VERSION = "0.2.0";
|
|
3
3
|
type Listener = (event: DL2LinkEvent) => void;
|
|
4
4
|
type Parsed = {
|
|
5
5
|
scheme: string;
|