@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.
@@ -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 and Expo.
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, or a bare React Native app with Expo Modules installed.
15
- - A development build. **Expo Go does not work**, because the SDK has native code and your app needs its own associated domains.
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 arrive twice. Events are stamped with the time the server receives them.
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
 
@@ -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.1.0'
20
+ version = '0.2.0'
5
21
 
6
- def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
- apply from: expoModulesCorePlugin
8
- applyKotlinExpoModulesCorePlugin()
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
- versionCode 1
17
- versionName "0.1.0"
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 expo.modules.kotlin.Promise
9
- import expo.modules.kotlin.exception.Exceptions
10
- import expo.modules.kotlin.modules.Module
11
- import expo.modules.kotlin.modules.ModuleDefinition
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 and the Play Install Referrer. Nothing else.
18
- class DL2Module : Module() {
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() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
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 definition() = ModuleDefinition {
26
- Name("DL2")
28
+ override fun getName() = "DL2"
27
29
 
28
- AsyncFunction("getItem") { key: String ->
29
- prefs.getString(key, null)
30
- }
30
+ @ReactMethod
31
+ fun getItem(key: String, promise: Promise) {
32
+ settle(promise) { prefs.getString(key, null) }
33
+ }
31
34
 
32
- AsyncFunction("setItem") { key: String, value: String ->
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
- AsyncFunction("removeItem") { key: String ->
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
- AsyncFunction("uuid") {
41
- UUID.randomUUID().toString()
42
- }
51
+ @ReactMethod
52
+ fun uuid(promise: Promise) {
53
+ settle(promise) { UUID.randomUUID().toString() }
54
+ }
43
55
 
44
- AsyncFunction("getDeviceInfo") {
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
- mapOf(
48
- "osVersion" to (Build.VERSION.RELEASE ?: ""),
49
- "appId" to ctx.packageName,
50
- "appVersion" to (info.versionName ?: ""),
51
- "lang" to Locale.getDefault().toLanguageTag(),
52
- "tz" to TimeZone.getDefault().id
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
- // Resolves with the referrer string, or null when Play has none. Read once, then cached.
57
- AsyncFunction("getInstallReferrer") { promise: Promise ->
58
- val cached = prefs.getString(REFERRER_KEY, null)
59
- if (cached != null) {
60
- promise.resolve(cached)
61
- return@AsyncFunction
62
- }
63
- val settled = AtomicBoolean(false)
64
- val finish = { value: String? ->
65
- if (settled.compareAndSet(false, true)) {
66
- promise.resolve(value)
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
- val client = InstallReferrerClient.newBuilder(context).build()
71
- client.startConnection(object : InstallReferrerStateListener {
72
- override fun onInstallReferrerSetupFinished(responseCode: Int) {
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 = client.installReferrer?.installReferrer
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
- try {
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
- override fun onInstallReferrerServiceDisconnected() {
94
- finish(null)
95
- }
96
- })
97
- } catch (e: Exception) {
98
- // No Play Store on this device (emulator image, Huawei, sideloaded build).
99
- finish(null)
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
- const wanted = `applinks:${d}${developerMode ? "?mode=developer" : ""}`;
102
- const i = out.findIndex((e) => e.split("?")[0].toLowerCase() === `applinks:${d}`);
103
- if (i < 0)
104
- out.push(wanted);
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);
@@ -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
- NativeView = (0, expo_modules_core_1.requireNativeViewManager)("DL2");
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;
@@ -1,5 +1,5 @@
1
1
  import type { DL2Config, DL2CreateLinkInput, DL2DebugInfo, DL2Deps, DL2LinkEvent, DL2TrackOptions } from "./types";
2
- export declare const SDK_VERSION = "0.1.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;