@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.
@@ -1,6 +1,6 @@
1
1
  require 'json'
2
2
 
3
- package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
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.dependency 'ExpoModulesCore'
17
+ s.source_files = 'ios/*.{h,m,swift}'
18
+ s.resource_bundles = { 'DL2ReactNative_Privacy' => ['ios/PrivacyInfo.xcprivacy'] }
19
19
 
20
- s.source_files = '*.swift'
21
- s.resource_bundles = { 'DL2ReactNative_Privacy' => ['PrivacyInfo.xcprivacy'] }
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 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. Version 0.1.1 was compiled for Android and iOS in a fresh Expo SDK 57 app.
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
@@ -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**
@@ -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.1'
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.1"
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 expo.modules.kotlin.Promise
10
- import expo.modules.kotlin.exception.Exceptions
11
- import expo.modules.kotlin.modules.Module
12
- 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
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 : Module() {
21
+ class DL2Module(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
21
22
  private val context: Context
22
- get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
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 definition() = ModuleDefinition {
28
- Name("DL2")
28
+ override fun getName() = "DL2"
29
29
 
30
- AsyncFunction("getItem") { key: String ->
31
- prefs.getString(key, null)
32
- }
30
+ @ReactMethod
31
+ fun getItem(key: String, promise: Promise) {
32
+ settle(promise) { prefs.getString(key, null) }
33
+ }
33
34
 
34
- AsyncFunction("setItem") { key: String, value: String ->
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
- AsyncFunction("removeItem") { key: String ->
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
- AsyncFunction("uuid") {
43
- UUID.randomUUID().toString()
44
- }
51
+ @ReactMethod
52
+ fun uuid(promise: Promise) {
53
+ settle(promise) { UUID.randomUUID().toString() }
54
+ }
45
55
 
46
- AsyncFunction("getDeviceInfo") {
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
- mapOf(
50
- "osVersion" to (Build.VERSION.RELEASE ?: ""),
51
- "appId" to ctx.packageName,
52
- "appVersion" to (info.versionName ?: ""),
53
- "lang" to Locale.getDefault().toLanguageTag(),
54
- "tz" to TimeZone.getDefault().id
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
- // When the app was installed, in ms since epoch. An update keeps it; a reinstall or a
59
- // restore onto another device does not.
60
- AsyncFunction("getInstalledAt") {
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
- // True when the activity was reopened from Recents: its intent is the one from the original
66
- // launch, so the URL in it was already delivered.
67
- AsyncFunction("isLaunchedFromHistory") {
68
- val flags = appContext.currentActivity?.intent?.flags ?: 0
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
- // Resolves with the referrer string, or null when Play has none. Read once, then cached.
73
- AsyncFunction("getInstallReferrer") { promise: Promise ->
74
- val cached = prefs.getString(REFERRER_KEY, null)
75
- if (cached != null) {
76
- promise.resolve(cached)
77
- return@AsyncFunction
78
- }
79
- val settled = AtomicBoolean(false)
80
- val finish = { value: String? ->
81
- if (settled.compareAndSet(false, true)) {
82
- promise.resolve(value)
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
- val c = InstallReferrerClient.newBuilder(context).build()
95
- client = c
96
- c.startConnection(object : InstallReferrerStateListener {
97
- override fun onInstallReferrerSetupFinished(responseCode: Int) {
98
- // Called on the main thread, and reading the referrer is a call into the Play
99
- // Store's process: do it on a thread of its own.
100
- Thread {
101
- var referrer: String? = null
102
- try {
103
- if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
104
- referrer = c.installReferrer?.installReferrer
105
- if (referrer != null) {
106
- prefs.edit().putString(REFERRER_KEY, referrer).apply()
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
- finish(referrer)
115
- }.start()
116
- }
129
+ } catch (e: Exception) {
130
+ referrer = null
131
+ } finally {
132
+ end()
133
+ }
134
+ finish(referrer)
135
+ }.start()
136
+ }
117
137
 
118
- override fun onInstallReferrerServiceDisconnected() {
119
- end()
120
- finish(null)
121
- }
122
- })
123
- } catch (e: Exception) {
124
- // No Play Store on this device (emulator image, Huawei, sideloaded build).
125
- end()
126
- finish(null)
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
+ }
@@ -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.1";
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.1.1";
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) {
@@ -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 (0, expo_modules_core_1.requireOptionalNativeModule)("DL2");
9
+ return react_native_1.NativeModules.DL2 || null;
10
10
  }
11
11
  catch {
12
12
  return null;
@@ -75,7 +75,7 @@ export type DL2DeviceInfo = {
75
75
  lang?: string;
76
76
  tz?: string;
77
77
  };
78
- /** The native module (Expo Modules API). Every function is asynchronous. */
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>;
@@ -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
@@ -1,59 +1,72 @@
1
- import ExpoModulesCore
1
+ import Foundation
2
+ import React
2
3
  import UIKit
3
4
 
4
- // Key-value storage, device info, the install time and the system paste button. Nothing else.
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
- public class DL2Module: Module {
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 definition() -> ModuleDefinition {
12
- Name("DL2")
13
+ @objc public static func requiresMainQueueSetup() -> Bool {
14
+ return false
15
+ }
13
16
 
14
- AsyncFunction("getItem") { (key: String) -> String? in
15
- return self.store.string(forKey: key)
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
- AsyncFunction("setItem") { (key: String, value: String) in
19
- self.store.set(value, forKey: key)
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
- AsyncFunction("removeItem") { (key: String) in
23
- self.store.removeObject(forKey: key)
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
- // The install referrer is an Android (Google Play) concept.
27
- AsyncFunction("getInstallReferrer") { () -> String? in
28
- return nil
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
- AsyncFunction("uuid") { () -> String in
32
- return UUID().uuidString.lowercased()
33
- }
40
+ @objc(uuid:rejecter:)
41
+ public func uuid(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
42
+ resolve(UUID().uuidString.lowercased())
43
+ }
34
44
 
35
- AsyncFunction("getDeviceInfo") { () -> [String: String] in
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
- return [
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
- View(DL2PasteButtonView.self) {
56
- Events("onPaste")
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 ExpoModulesCore
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
- final class DL2PasteButtonView: ExpoView {
8
- let onPaste = EventDispatcher()
8
+ @objc(DL2PasteButtonView)
9
+ public final class DL2PasteButtonView: UIView {
10
+ @objc public var onPaste: RCTDirectEventBlock?
9
11
  private var control: UIView?
10
12
 
11
- required init(appContext: AppContext? = nil) {
12
- super.init(appContext: appContext)
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
- override func layoutSubviews() {
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.1.1",
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
- "expo-module.config.json",
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
  }
@@ -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
- NativeView = requireNativeViewManager<NativeProps>("DL2");
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.1.1";
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 requireOptionalNativeModule<DL2Native>("DL2");
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 (Expo Modules API). Every function is asynchronous. */
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>;
@@ -1,5 +0,0 @@
1
- {
2
- "platforms": ["ios", "android"],
3
- "ios": { "modules": ["DL2Module"] },
4
- "android": { "modules": ["app.dl2.reactnative.DL2Module"] }
5
- }