@dl2app/react-native 0.1.1 → 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/{ios/DL2ReactNative.podspec → DL2ReactNative.podspec} +9 -5
- package/README.md +54 -31
- package/android/build.gradle +30 -9
- package/android/src/main/java/app/dl2/reactnative/DL2Module.kt +109 -82
- package/android/src/main/java/app/dl2/reactnative/DL2Package.kt +17 -0
- package/build/src/PasteButton.js +2 -2
- package/build/src/core.d.ts +1 -1
- package/build/src/core.js +2 -2
- package/build/src/index.js +2 -2
- package/build/src/types.d.ts +1 -1
- package/ios/DL2Module.m +22 -0
- package/ios/DL2Module.swift +48 -35
- package/ios/DL2PasteButtonView.swift +25 -8
- package/package.json +5 -3
- package/src/PasteButton.ts +3 -3
- package/src/core.ts +2 -2
- package/src/index.ts +3 -3
- package/src/types.ts +1 -1
- package/expo-module.config.json +0 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
|
|
3
|
-
package = JSON.parse(File.read(File.join(__dir__, '
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
4
|
|
|
5
5
|
Pod::Spec.new do |s|
|
|
6
6
|
s.name = 'DL2ReactNative'
|
|
@@ -13,10 +13,14 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.platforms = { :ios => '13.4' }
|
|
14
14
|
s.swift_version = '5.4'
|
|
15
15
|
s.source = { :git => 'https://github.com/dl2app/dl2-react-native.git', :tag => "v#{s.version}" }
|
|
16
|
-
s.static_framework = true
|
|
17
16
|
|
|
18
|
-
s.
|
|
17
|
+
s.source_files = 'ios/*.{h,m,swift}'
|
|
18
|
+
s.resource_bundles = { 'DL2ReactNative_Privacy' => ['ios/PrivacyInfo.xcprivacy'] }
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
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
|
-
-
|
|
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
|
|
@@ -53,33 +103,6 @@ The plugin adds to `ios.associatedDomains`, `android.intentFilters` and `scheme`
|
|
|
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
|
|
@@ -249,7 +272,7 @@ On iOS development builds, set `iosDeveloperMode: true` and turn on Settings, De
|
|
|
249
272
|
**Nothing happens and there is no error**
|
|
250
273
|
|
|
251
274
|
- Turn on `debug: true` and read the `[DL2]` lines.
|
|
252
|
-
- "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.
|
|
253
276
|
- The host of the URL is not in `linkDomains`. The SDK ignores URLs on other hosts.
|
|
254
277
|
|
|
255
278
|
**Deferred links do not match on iOS**
|
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
|
}
|
|
@@ -6,10 +6,11 @@ import android.content.SharedPreferences
|
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import com.android.installreferrer.api.InstallReferrerClient
|
|
8
8
|
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
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
|
|
13
14
|
import java.util.Locale
|
|
14
15
|
import java.util.TimeZone
|
|
15
16
|
import java.util.UUID
|
|
@@ -17,114 +18,140 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
17
18
|
|
|
18
19
|
// Key-value storage, device info, the install time, the launch intent's history flag and the
|
|
19
20
|
// Play Install Referrer. Nothing else.
|
|
20
|
-
class DL2Module :
|
|
21
|
+
class DL2Module(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
21
22
|
private val context: Context
|
|
22
|
-
get() =
|
|
23
|
+
get() = reactApplicationContext
|
|
23
24
|
|
|
24
25
|
private val prefs: SharedPreferences
|
|
25
26
|
get() = context.getSharedPreferences("app.dl2.sdk", Context.MODE_PRIVATE)
|
|
26
27
|
|
|
27
|
-
override fun
|
|
28
|
-
Name("DL2")
|
|
28
|
+
override fun getName() = "DL2"
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
30
|
+
@ReactMethod
|
|
31
|
+
fun getItem(key: String, promise: Promise) {
|
|
32
|
+
settle(promise) { prefs.getString(key, null) }
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
@ReactMethod
|
|
36
|
+
fun setItem(key: String, value: String, promise: Promise) {
|
|
37
|
+
settle(promise) {
|
|
35
38
|
prefs.edit().putString(key, value).apply()
|
|
39
|
+
null
|
|
36
40
|
}
|
|
41
|
+
}
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
@ReactMethod
|
|
44
|
+
fun removeItem(key: String, promise: Promise) {
|
|
45
|
+
settle(promise) {
|
|
39
46
|
prefs.edit().remove(key).apply()
|
|
47
|
+
null
|
|
40
48
|
}
|
|
49
|
+
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
51
|
+
@ReactMethod
|
|
52
|
+
fun uuid(promise: Promise) {
|
|
53
|
+
settle(promise) { UUID.randomUUID().toString() }
|
|
54
|
+
}
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
@ReactMethod
|
|
57
|
+
fun getDeviceInfo(promise: Promise) {
|
|
58
|
+
settle(promise) {
|
|
47
59
|
val ctx = context
|
|
48
60
|
val info = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
|
49
|
-
|
|
50
|
-
"osVersion"
|
|
51
|
-
"appId"
|
|
52
|
-
"appVersion"
|
|
53
|
-
"lang"
|
|
54
|
-
"tz"
|
|
55
|
-
|
|
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
|
+
}
|
|
56
68
|
}
|
|
69
|
+
}
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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) {
|
|
61
76
|
val ctx = context
|
|
62
77
|
ctx.packageManager.getPackageInfo(ctx.packageName, 0).firstInstallTime.toDouble()
|
|
63
78
|
}
|
|
79
|
+
}
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
69
87
|
(flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0
|
|
70
88
|
}
|
|
89
|
+
}
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
var client: InstallReferrerClient? = null
|
|
86
|
-
val end = {
|
|
87
|
-
try {
|
|
88
|
-
client?.endConnection()
|
|
89
|
-
} catch (e: Exception) {
|
|
90
|
-
// already closed
|
|
91
|
-
}
|
|
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)
|
|
92
103
|
}
|
|
104
|
+
}
|
|
105
|
+
var client: InstallReferrerClient? = null
|
|
106
|
+
val end = {
|
|
93
107
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|
|
121
|
+
var referrer: String? = null
|
|
122
|
+
try {
|
|
123
|
+
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
124
|
+
referrer = c.installReferrer?.installReferrer
|
|
125
|
+
if (referrer != null) {
|
|
126
|
+
prefs.edit().putString(REFERRER_KEY, referrer).apply()
|
|
108
127
|
}
|
|
109
|
-
} catch (e: Exception) {
|
|
110
|
-
referrer = null
|
|
111
|
-
} finally {
|
|
112
|
-
end()
|
|
113
128
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
} catch (e: Exception) {
|
|
130
|
+
referrer = null
|
|
131
|
+
} finally {
|
|
132
|
+
end()
|
|
133
|
+
}
|
|
134
|
+
finish(referrer)
|
|
135
|
+
}.start()
|
|
136
|
+
}
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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)
|
|
128
155
|
}
|
|
129
156
|
}
|
|
130
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
|
+
}
|
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;
|
package/build/src/core.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.SDK_VERSION = void 0;
|
|
|
4
4
|
exports.parseUrl = parseUrl;
|
|
5
5
|
exports.extractClipboardToken = extractClipboardToken;
|
|
6
6
|
exports.createDL2 = createDL2;
|
|
7
|
-
exports.SDK_VERSION = "0.
|
|
7
|
+
exports.SDK_VERSION = "0.2.0";
|
|
8
8
|
const K_ID = "install_id";
|
|
9
9
|
const K_DONE = "first_launch_done";
|
|
10
10
|
const K_ATTEMPTS = "open_attempts";
|
|
@@ -528,7 +528,7 @@ function createDL2(deps) {
|
|
|
528
528
|
queue = old.concat(queue).slice(-MAX_QUEUED_EVENTS);
|
|
529
529
|
}
|
|
530
530
|
if (!deps.native) {
|
|
531
|
-
report(mkErr("unknown", "DL2 native module not found. Rebuild the app (Expo Go is not supported); deferred links are off."), "configure");
|
|
531
|
+
report(mkErr("unknown", "DL2 native module not found. Rebuild the app (on iOS run pod install first; Expo Go is not supported); deferred links are off."), "configure");
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
async function runInitial(g) {
|
package/build/src/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SDK_VERSION = exports.extractClipboardToken = exports.parseUrl = exports.DL2PasteButton = exports.DL2 = void 0;
|
|
4
4
|
const react_native_1 = require("react-native");
|
|
5
|
-
const expo_modules_core_1 = require("expo-modules-core");
|
|
6
5
|
const core_1 = require("./core");
|
|
6
|
+
// A plain React Native module, linked by autolinking in bare React Native and in Expo alike.
|
|
7
7
|
function loadNative() {
|
|
8
8
|
try {
|
|
9
|
-
return
|
|
9
|
+
return react_native_1.NativeModules.DL2 || null;
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
12
|
return null;
|
package/build/src/types.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type DL2DeviceInfo = {
|
|
|
75
75
|
lang?: string;
|
|
76
76
|
tz?: string;
|
|
77
77
|
};
|
|
78
|
-
/** The native module (
|
|
78
|
+
/** The native module (a plain React Native module). Every function is asynchronous. */
|
|
79
79
|
export interface DL2Native {
|
|
80
80
|
getItem(key: string): Promise<string | null>;
|
|
81
81
|
setItem(key: string, value: string): Promise<void>;
|
package/ios/DL2Module.m
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTViewManager.h>
|
|
3
|
+
|
|
4
|
+
// Exposes the Swift classes to React Native. The implementations are in DL2Module.swift and
|
|
5
|
+
// DL2PasteButtonView.swift.
|
|
6
|
+
@interface RCT_EXTERN_REMAP_MODULE(DL2, DL2ReactNativeModule, NSObject)
|
|
7
|
+
|
|
8
|
+
RCT_EXTERN_METHOD(getItem:(NSString *)key resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
RCT_EXTERN_METHOD(setItem:(NSString *)key value:(NSString *)value resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
RCT_EXTERN_METHOD(removeItem:(NSString *)key resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
RCT_EXTERN_METHOD(getInstallReferrer:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
RCT_EXTERN_METHOD(uuid:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
14
|
+
RCT_EXTERN_METHOD(getInstalledAt:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
@end
|
|
17
|
+
|
|
18
|
+
@interface RCT_EXTERN_REMAP_MODULE(DL2PasteButton, DL2PasteButtonManager, RCTViewManager)
|
|
19
|
+
|
|
20
|
+
RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock)
|
|
21
|
+
|
|
22
|
+
@end
|
package/ios/DL2Module.swift
CHANGED
|
@@ -1,59 +1,72 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
4
|
-
// Key-value storage, device info
|
|
5
|
+
// Key-value storage, device info and the install time. Nothing else.
|
|
5
6
|
// The clipboard is never read here: text only arrives when the user taps DL2PasteButtonView.
|
|
6
|
-
|
|
7
|
+
@objc(DL2ReactNativeModule)
|
|
8
|
+
public class DL2Module: NSObject {
|
|
7
9
|
private var store: UserDefaults {
|
|
8
10
|
return UserDefaults(suiteName: "app.dl2.sdk") ?? UserDefaults.standard
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
public func
|
|
12
|
-
|
|
13
|
+
@objc public static func requiresMainQueueSetup() -> Bool {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
@objc(getItem:resolver:rejecter:)
|
|
18
|
+
public func getItem(_ key: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
19
|
+
resolve(store.string(forKey: key))
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
@objc(setItem:value:resolver:rejecter:)
|
|
23
|
+
public func setItem(_ key: String, value: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
24
|
+
store.set(value, forKey: key)
|
|
25
|
+
resolve(nil)
|
|
26
|
+
}
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
@objc(removeItem:resolver:rejecter:)
|
|
29
|
+
public func removeItem(_ key: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
30
|
+
store.removeObject(forKey: key)
|
|
31
|
+
resolve(nil)
|
|
32
|
+
}
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
// The install referrer is an Android (Google Play) concept.
|
|
35
|
+
@objc(getInstallReferrer:rejecter:)
|
|
36
|
+
public func getInstallReferrer(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
37
|
+
resolve(nil)
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
@objc(uuid:rejecter:)
|
|
41
|
+
public func uuid(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
42
|
+
resolve(UUID().uuidString.lowercased())
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
@objc(getDeviceInfo:rejecter:)
|
|
46
|
+
public func getDeviceInfo(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
47
|
+
// UIDevice belongs to the main thread; module methods run on a background queue.
|
|
48
|
+
DispatchQueue.main.async {
|
|
36
49
|
let bundle = Bundle.main
|
|
37
|
-
|
|
50
|
+
resolve([
|
|
38
51
|
"osVersion": UIDevice.current.systemVersion,
|
|
39
52
|
"appId": bundle.bundleIdentifier ?? "",
|
|
40
53
|
"appVersion": (bundle.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "",
|
|
41
54
|
"lang": Locale.preferredLanguages.first ?? "",
|
|
42
55
|
"tz": TimeZone.current.identifier
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// When the app was installed, in ms since epoch. iOS has no API for it; the data
|
|
47
|
-
// container's Library directory is created with the install. Nil when it cannot be read.
|
|
48
|
-
AsyncFunction("getInstalledAt") { () -> Double? in
|
|
49
|
-
guard let dir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first,
|
|
50
|
-
let created = (try? FileManager.default.attributesOfItem(atPath: dir.path))?[.creationDate] as? Date
|
|
51
|
-
else { return nil }
|
|
52
|
-
return (created.timeIntervalSince1970 * 1000).rounded()
|
|
56
|
+
])
|
|
53
57
|
}
|
|
58
|
+
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// When the app was installed, in ms since epoch. iOS has no API for it; the data
|
|
61
|
+
// container's Library directory is created with the install. Nil when it cannot be read.
|
|
62
|
+
@objc(getInstalledAt:rejecter:)
|
|
63
|
+
public func getInstalledAt(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
64
|
+
guard let dir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first,
|
|
65
|
+
let created = (try? FileManager.default.attributesOfItem(atPath: dir.path))?[.creationDate] as? Date
|
|
66
|
+
else {
|
|
67
|
+
resolve(nil)
|
|
68
|
+
return
|
|
57
69
|
}
|
|
70
|
+
resolve((created.timeIntervalSince1970 * 1000).rounded())
|
|
58
71
|
}
|
|
59
72
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
4
5
|
// Hosts the system paste button (UIPasteControl, iOS 16+). iOS gives the app the clipboard
|
|
5
6
|
// content only after the user taps it, without the "pasted from" banner or a permission alert.
|
|
6
7
|
// On older iOS the view stays empty.
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
@objc(DL2PasteButtonView)
|
|
9
|
+
public final class DL2PasteButtonView: UIView {
|
|
10
|
+
@objc public var onPaste: RCTDirectEventBlock?
|
|
9
11
|
private var control: UIView?
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
super.init(
|
|
13
|
+
public override init(frame: CGRect) {
|
|
14
|
+
super.init(frame: frame)
|
|
13
15
|
pasteConfiguration = UIPasteConfiguration(acceptableTypeIdentifiers: ["public.plain-text", "public.url"])
|
|
14
16
|
if #available(iOS 16.0, *) {
|
|
15
17
|
let configuration = UIPasteControl.Configuration()
|
|
@@ -22,20 +24,35 @@ final class DL2PasteButtonView: ExpoView {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
required init?(coder: NSCoder) {
|
|
28
|
+
fatalError("init(coder:) is not used")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public override func layoutSubviews() {
|
|
26
32
|
super.layoutSubviews()
|
|
27
33
|
control?.frame = bounds
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
override func paste(itemProviders: [NSItemProvider]) {
|
|
36
|
+
public override func paste(itemProviders: [NSItemProvider]) {
|
|
31
37
|
guard let provider = itemProviders.first(where: { $0.canLoadObject(ofClass: NSString.self) }) else {
|
|
32
38
|
return
|
|
33
39
|
}
|
|
34
40
|
_ = provider.loadObject(ofClass: NSString.self) { [weak self] value, _ in
|
|
35
41
|
guard let text = value as? String else { return }
|
|
36
42
|
DispatchQueue.main.async {
|
|
37
|
-
self?.onPaste(["text": text])
|
|
43
|
+
self?.onPaste?(["text": text])
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
@objc(DL2PasteButtonManager)
|
|
50
|
+
public final class DL2PasteButtonManager: RCTViewManager {
|
|
51
|
+
public override static func requiresMainQueueSetup() -> Bool {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public override func view() -> UIView! {
|
|
56
|
+
return DL2PasteButtonView()
|
|
57
|
+
}
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dl2app/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DL2App deep links for React Native and Expo: universal links, app links, deferred deep links and events.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://dl2.app/docs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build",
|
|
14
14
|
"plugin/src",
|
|
15
15
|
"app.plugin.js",
|
|
16
|
-
"
|
|
16
|
+
"DL2ReactNative.podspec",
|
|
17
17
|
"ios",
|
|
18
18
|
"android/build.gradle",
|
|
19
19
|
"android/src",
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"keywords": ["react-native", "expo", "deep-linking", "universal-links", "app-links", "deferred-deep-link"],
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"expo": ">=51.0.0",
|
|
30
|
-
"expo-modules-core": "*",
|
|
31
30
|
"react": "*",
|
|
32
31
|
"react-native": ">=0.74.0"
|
|
33
32
|
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"expo": { "optional": true }
|
|
35
|
+
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"typescript": "^5.9.3"
|
|
36
38
|
}
|
package/src/PasteButton.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createElement, type ComponentType, type ReactElement } from "react";
|
|
2
|
-
import { Platform } from "react-native";
|
|
3
|
-
import { requireNativeViewManager } from "expo-modules-core";
|
|
2
|
+
import { Platform, requireNativeComponent, UIManager } from "react-native";
|
|
4
3
|
|
|
5
4
|
export type DL2PasteButtonProps = {
|
|
6
5
|
/** Called with the pasted text after the user taps the button. */
|
|
@@ -16,7 +15,8 @@ let NativeView: ComponentType<NativeProps> | null | undefined;
|
|
|
16
15
|
function nativeView(): ComponentType<NativeProps> | null {
|
|
17
16
|
if (NativeView === undefined) {
|
|
18
17
|
try {
|
|
19
|
-
|
|
18
|
+
// Asking for a view the app was not built with fails only when it renders, so check first.
|
|
19
|
+
NativeView = UIManager.hasViewManagerConfig("DL2PasteButton") ? requireNativeComponent<NativeProps>("DL2PasteButton") : null;
|
|
20
20
|
} catch {
|
|
21
21
|
NativeView = null;
|
|
22
22
|
}
|
package/src/core.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
DL2TrackOptions,
|
|
12
12
|
} from "./types";
|
|
13
13
|
|
|
14
|
-
export const SDK_VERSION = "0.
|
|
14
|
+
export const SDK_VERSION = "0.2.0";
|
|
15
15
|
|
|
16
16
|
const K_ID = "install_id";
|
|
17
17
|
const K_DONE = "first_launch_done";
|
|
@@ -540,7 +540,7 @@ export function createDL2(deps: DL2Deps) {
|
|
|
540
540
|
}
|
|
541
541
|
if (!deps.native) {
|
|
542
542
|
report(
|
|
543
|
-
mkErr("unknown", "DL2 native module not found. Rebuild the app (Expo Go is not supported); deferred links are off."),
|
|
543
|
+
mkErr("unknown", "DL2 native module not found. Rebuild the app (on iOS run pod install first; Expo Go is not supported); deferred links are off."),
|
|
544
544
|
"configure",
|
|
545
545
|
);
|
|
546
546
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Dimensions, Linking, PixelRatio, Platform } from "react-native";
|
|
2
|
-
import { requireOptionalNativeModule } from "expo-modules-core";
|
|
1
|
+
import { Dimensions, Linking, NativeModules, PixelRatio, Platform } from "react-native";
|
|
3
2
|
import { createDL2 } from "./core";
|
|
4
3
|
import type { DL2Native } from "./types";
|
|
5
4
|
|
|
5
|
+
// A plain React Native module, linked by autolinking in bare React Native and in Expo alike.
|
|
6
6
|
function loadNative(): DL2Native | null {
|
|
7
7
|
try {
|
|
8
|
-
return
|
|
8
|
+
return (NativeModules.DL2 as DL2Native | undefined) || null;
|
|
9
9
|
} catch {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
package/src/types.ts
CHANGED
|
@@ -84,7 +84,7 @@ export type DL2DeviceInfo = {
|
|
|
84
84
|
tz?: string;
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
/** The native module (
|
|
87
|
+
/** The native module (a plain React Native module). Every function is asynchronous. */
|
|
88
88
|
export interface DL2Native {
|
|
89
89
|
getItem(key: string): Promise<string | null>;
|
|
90
90
|
setItem(key: string, value: string): Promise<void>;
|