@bdmakers/react-native-deferred-link 1.0.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,20 @@
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 = "DeferredLink"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/bd-makers/react-native-deferred-link.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 aijinet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @bdmakers/react-native-deferred-link
2
+
3
+ Deferred deep link recovery for React Native
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @bdmakers/react-native-deferred-link
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { DeferredLink } from '@bdmakers/react-native-deferred-link';
15
+
16
+ // Configure (call once at app startup)
17
+ DeferredLink.configure({
18
+ domains: ['your-domain.com'],
19
+ appScheme: 'yourapp',
20
+ });
21
+
22
+ // Get deferred link on first launch
23
+ const result = await DeferredLink.getInitialDeferredLink();
24
+ if (result.found) {
25
+ console.log('Deferred link URL:', result.url);
26
+ console.log('Source:', result.source); // 'android_install_referrer' | 'ios_pasteboard'
27
+ }
28
+
29
+ // Clear consumed link
30
+ await DeferredLink.clearConsumedDeferredLink();
31
+ ```
32
+
33
+ ## Contributing
34
+
35
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
36
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
37
+ - [Code of conduct](CODE_OF_CONDUCT.md)
38
+
39
+ ## License
40
+
41
+ MIT
42
+
43
+ ---
44
+
45
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,71 @@
1
+ buildscript {
2
+ ext.DeferredLink = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return DeferredLink[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ def isNewArchEnabled = rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
31
+
32
+ apply plugin: "com.android.library"
33
+ apply plugin: "kotlin-android"
34
+
35
+ apply plugin: "com.facebook.react"
36
+
37
+ android {
38
+ namespace "com.deferredlink"
39
+
40
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
41
+
42
+ defaultConfig {
43
+ minSdkVersion getExtOrDefault("minSdkVersion")
44
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
45
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchEnabled.toString()
46
+ }
47
+
48
+ buildFeatures {
49
+ buildConfig true
50
+ }
51
+
52
+ buildTypes {
53
+ release {
54
+ minifyEnabled false
55
+ }
56
+ }
57
+
58
+ lint {
59
+ disable "GradleCompatible"
60
+ }
61
+
62
+ compileOptions {
63
+ sourceCompatibility JavaVersion.VERSION_1_8
64
+ targetCompatibility JavaVersion.VERSION_1_8
65
+ }
66
+ }
67
+
68
+ dependencies {
69
+ implementation "com.facebook.react:react-android"
70
+ implementation "com.android.installreferrer:installreferrer:2.2"
71
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,134 @@
1
+ package com.deferredlink
2
+
3
+ import android.net.Uri
4
+ import android.util.Log
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.bridge.WritableMap
10
+
11
+ class DeferredLinkModule(reactContext: ReactApplicationContext) :
12
+ NativeDeferredLinkSpec(reactContext) {
13
+
14
+ private val storage = DeferredLinkStorage(reactContext)
15
+ private val referrerParser = ReferrerParser(reactContext)
16
+
17
+ private var configDomains: List<String> = emptyList()
18
+ private var configAppScheme: String? = null
19
+ private var configReferrerParamKey: String = DEFAULT_REFERRER_PARAM_KEY
20
+
21
+ override fun configure(config: ReadableMap) {
22
+ if (config.hasKey("domains")) {
23
+ val domainsArray = config.getArray("domains")
24
+ val domains = mutableListOf<String>()
25
+ if (domainsArray != null) {
26
+ for (i in 0 until domainsArray.size()) {
27
+ domainsArray.getString(i)?.let { domains.add(it) }
28
+ }
29
+ }
30
+ configDomains = domains
31
+ }
32
+
33
+ if (config.hasKey("appScheme")) {
34
+ configAppScheme = config.getString("appScheme")
35
+ }
36
+
37
+ if (config.hasKey("android")) {
38
+ val androidConfig = config.getMap("android")
39
+ if (androidConfig?.hasKey("installReferrerParamKey") == true) {
40
+ configReferrerParamKey =
41
+ androidConfig.getString("installReferrerParamKey") ?: DEFAULT_REFERRER_PARAM_KEY
42
+ }
43
+ }
44
+ }
45
+
46
+ override fun getInitialDeferredLink(promise: Promise) {
47
+ if (storage.consumed) {
48
+ promise.resolve(buildNotFoundResult())
49
+ return
50
+ }
51
+
52
+ referrerParser.fetchDeferredLink(configReferrerParamKey) { referrerResult ->
53
+ if (referrerResult == null) {
54
+ promise.resolve(buildNotFoundResult())
55
+ return@fetchDeferredLink
56
+ }
57
+
58
+ val url = referrerResult.url
59
+
60
+ if (!isDomainAllowed(url)) {
61
+ Log.d(TAG, "Domain not in allowed list: $url")
62
+ promise.resolve(buildNotFoundResult())
63
+ return@fetchDeferredLink
64
+ }
65
+
66
+ storage.markConsumed(url)
67
+
68
+ val result = Arguments.createMap().apply {
69
+ putBoolean("found", true)
70
+ putString("source", "android_install_referrer")
71
+ putString("url", url)
72
+ putString("rawValue", referrerResult.rawReferrer)
73
+ if (referrerResult.clickedAtSeconds > 0) {
74
+ putDouble("clickedAt", referrerResult.clickedAtSeconds.toDouble())
75
+ }
76
+ val metadata = parseUrlMetadata(url)
77
+ if (metadata != null) {
78
+ putMap("metadata", metadata)
79
+ }
80
+ }
81
+ promise.resolve(result)
82
+ }
83
+ }
84
+
85
+ override fun clearConsumedDeferredLink(promise: Promise) {
86
+ storage.clear()
87
+ promise.resolve(null)
88
+ }
89
+
90
+ private fun isDomainAllowed(url: String): Boolean {
91
+ if (configDomains.isEmpty()) return true
92
+ return try {
93
+ val host = Uri.parse(url).host ?: return false
94
+ configDomains.any { domain ->
95
+ host == domain || host.endsWith(".$domain")
96
+ }
97
+ } catch (e: Exception) {
98
+ Log.w(TAG, "Failed to parse URL for domain check", e)
99
+ false
100
+ }
101
+ }
102
+
103
+ private fun parseUrlMetadata(url: String): WritableMap? {
104
+ return try {
105
+ val uri = Uri.parse(url)
106
+ val paramNames = uri.queryParameterNames
107
+ if (paramNames.isEmpty()) return null
108
+ Arguments.createMap().apply {
109
+ for (key in paramNames) {
110
+ val value = uri.getQueryParameter(key)
111
+ if (value != null) {
112
+ putString(key, value)
113
+ }
114
+ }
115
+ }
116
+ } catch (e: Exception) {
117
+ Log.w(TAG, "Failed to parse URL metadata", e)
118
+ null
119
+ }
120
+ }
121
+
122
+ private fun buildNotFoundResult(): ReadableMap {
123
+ return Arguments.createMap().apply {
124
+ putBoolean("found", false)
125
+ putString("source", "none")
126
+ }
127
+ }
128
+
129
+ companion object {
130
+ const val NAME = NativeDeferredLinkSpec.NAME
131
+ private const val TAG = "DeferredLinkModule"
132
+ private const val DEFAULT_REFERRER_PARAM_KEY = "ddl"
133
+ }
134
+ }
@@ -0,0 +1,31 @@
1
+ package com.deferredlink
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class DeferredLinkPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == DeferredLinkModule.NAME) {
13
+ DeferredLinkModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ DeferredLinkModule.NAME to ReactModuleInfo(
22
+ name = DeferredLinkModule.NAME,
23
+ className = DeferredLinkModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
28
+ )
29
+ )
30
+ }
31
+ }
@@ -0,0 +1,45 @@
1
+ package com.deferredlink
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+
6
+ class DeferredLinkStorage(context: Context) {
7
+
8
+ private val prefs: SharedPreferences =
9
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
10
+
11
+ var consumed: Boolean
12
+ get() = prefs.getBoolean(KEY_CONSUMED, false)
13
+ set(value) = prefs.edit().putBoolean(KEY_CONSUMED, value).apply()
14
+
15
+ var lastValue: String?
16
+ get() = prefs.getString(KEY_LAST_VALUE, null)
17
+ set(value) = prefs.edit().putString(KEY_LAST_VALUE, value).apply()
18
+
19
+ var lastConsumedAt: Long
20
+ get() = prefs.getLong(KEY_LAST_CONSUMED_AT, 0L)
21
+ set(value) = prefs.edit().putLong(KEY_LAST_CONSUMED_AT, value).apply()
22
+
23
+ fun markConsumed(url: String) {
24
+ prefs.edit()
25
+ .putBoolean(KEY_CONSUMED, true)
26
+ .putString(KEY_LAST_VALUE, url)
27
+ .putLong(KEY_LAST_CONSUMED_AT, System.currentTimeMillis())
28
+ .apply()
29
+ }
30
+
31
+ fun clear() {
32
+ prefs.edit()
33
+ .remove(KEY_CONSUMED)
34
+ .remove(KEY_LAST_VALUE)
35
+ .remove(KEY_LAST_CONSUMED_AT)
36
+ .apply()
37
+ }
38
+
39
+ companion object {
40
+ private const val PREFS_NAME = "deferred_link_storage"
41
+ private const val KEY_CONSUMED = "deferred_link.consumed"
42
+ private const val KEY_LAST_VALUE = "deferred_link.last_value"
43
+ private const val KEY_LAST_CONSUMED_AT = "deferred_link.last_consumed_at"
44
+ }
45
+ }
@@ -0,0 +1,77 @@
1
+ package com.deferredlink
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import com.android.installreferrer.api.InstallReferrerClient
7
+ import com.android.installreferrer.api.InstallReferrerStateListener
8
+
9
+ data class ReferrerResult(
10
+ val url: String,
11
+ val clickedAtSeconds: Long,
12
+ val rawReferrer: String
13
+ )
14
+
15
+ class ReferrerParser(private val context: Context) {
16
+
17
+ fun fetchDeferredLink(paramKey: String, onResult: (ReferrerResult?) -> Unit) {
18
+ val client = InstallReferrerClient.newBuilder(context).build()
19
+
20
+ client.startConnection(object : InstallReferrerStateListener {
21
+ override fun onInstallReferrerSetupFinished(responseCode: Int) {
22
+ when (responseCode) {
23
+ InstallReferrerClient.InstallReferrerResponse.OK -> {
24
+ try {
25
+ val details = client.installReferrer
26
+ val referrerString = details.installReferrer
27
+ val clickedAt = details.referrerClickTimestampSeconds
28
+ val url = extractDeferredLink(referrerString, paramKey)
29
+ if (url != null) {
30
+ onResult(ReferrerResult(url, clickedAt, referrerString))
31
+ } else {
32
+ onResult(null)
33
+ }
34
+ } catch (e: Exception) {
35
+ Log.w(TAG, "Failed to read install referrer", e)
36
+ onResult(null)
37
+ } finally {
38
+ client.endConnection()
39
+ }
40
+ }
41
+ InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED,
42
+ InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
43
+ Log.d(TAG, "Install referrer not available: responseCode=$responseCode")
44
+ onResult(null)
45
+ client.endConnection()
46
+ }
47
+ else -> {
48
+ Log.d(TAG, "Install referrer unknown response: $responseCode")
49
+ onResult(null)
50
+ client.endConnection()
51
+ }
52
+ }
53
+ }
54
+
55
+ override fun onInstallReferrerServiceDisconnected() {
56
+ Log.d(TAG, "Install referrer service disconnected")
57
+ }
58
+ })
59
+ }
60
+
61
+ private fun extractDeferredLink(referrerString: String, paramKey: String): String? {
62
+ if (referrerString.isBlank()) return null
63
+ return try {
64
+ // Referrer string format: URL-encoded query params (e.g. "utm_source=google&ddl=https%3A%2F%2F...")
65
+ val fakeUri = Uri.parse("https://dummy?$referrerString")
66
+ val encodedValue = fakeUri.getQueryParameter(paramKey)
67
+ if (encodedValue.isNullOrBlank()) null else encodedValue
68
+ } catch (e: Exception) {
69
+ Log.w(TAG, "Failed to parse referrer string", e)
70
+ null
71
+ }
72
+ }
73
+
74
+ companion object {
75
+ private const val TAG = "ReferrerParser"
76
+ }
77
+ }
@@ -0,0 +1,4 @@
1
+ #import <DeferredLinkSpec/DeferredLinkSpec.h>
2
+
3
+ @interface DeferredLink : NSObject <NativeDeferredLinkSpec>
4
+ @end
@@ -0,0 +1,288 @@
1
+ #import "DeferredLink.h"
2
+ #import <UIKit/UIKit.h>
3
+
4
+ static NSString *const kStorageSuiteKey = @"com.deferredlink.storage";
5
+ static NSString *const kConsumedKey = @"deferred_link.consumed";
6
+ static NSString *const kLastValueKey = @"deferred_link.last_value";
7
+ static NSString *const kLastConsumedAtKey = @"deferred_link.last_consumed_at";
8
+ static NSString *const kPayloadSeparator = @"|";
9
+
10
+ @implementation DeferredLink {
11
+ NSArray<NSString *> *_configDomains;
12
+ NSString *_configAppScheme;
13
+ NSString *_configPasteboardPrefix;
14
+ NSTimeInterval _configPasteboardTTLSeconds;
15
+ }
16
+
17
+ - (instancetype)init {
18
+ self = [super init];
19
+ if (self) {
20
+ _configDomains = @[];
21
+ _configAppScheme = nil;
22
+ _configPasteboardPrefix = @"bodoc:ddl:";
23
+ _configPasteboardTTLSeconds = 900;
24
+ NSLog(@"[DeferredLink] module initialized (defaults: prefix=%@, ttl=%.0f)",
25
+ _configPasteboardPrefix, _configPasteboardTTLSeconds);
26
+ }
27
+ return self;
28
+ }
29
+
30
+ RCT_EXPORT_MODULE(DeferredLink)
31
+
32
+ + (BOOL)requiresMainQueueSetup {
33
+ return NO;
34
+ }
35
+
36
+ #pragma mark - Public API
37
+
38
+ // configure — RCT_EXPORT_METHOD provides both bridge discovery and TurboModule compatibility
39
+ RCT_EXPORT_METHOD(configure:(NSDictionary *)config)
40
+ {
41
+ NSLog(@"[DeferredLink] configure called with: %@", config);
42
+
43
+ if (config[@"domains"]) {
44
+ _configDomains = config[@"domains"];
45
+ }
46
+ if (config[@"appScheme"]) {
47
+ _configAppScheme = config[@"appScheme"];
48
+ }
49
+ NSDictionary *iosConfig = config[@"ios"];
50
+ if (iosConfig) {
51
+ if (iosConfig[@"pasteboardPrefix"]) {
52
+ _configPasteboardPrefix = iosConfig[@"pasteboardPrefix"];
53
+ }
54
+ if (iosConfig[@"pasteboardTTLSeconds"]) {
55
+ _configPasteboardTTLSeconds = [iosConfig[@"pasteboardTTLSeconds"] doubleValue];
56
+ }
57
+ }
58
+
59
+ NSLog(@"[DeferredLink] configured — domains=%@, scheme=%@, prefix=%@, ttl=%.0f",
60
+ _configDomains, _configAppScheme, _configPasteboardPrefix, _configPasteboardTTLSeconds);
61
+ }
62
+
63
+ // getInitialDeferredLink — RCT_EXPORT_METHOD provides both bridge discovery and TurboModule compatibility
64
+ RCT_EXPORT_METHOD(getInitialDeferredLink:(RCTPromiseResolveBlock)resolve
65
+ reject:(RCTPromiseRejectBlock)reject)
66
+ {
67
+ NSLog(@"[DeferredLink] getInitialDeferredLink called (thread: %@)", [NSThread currentThread]);
68
+
69
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kStorageSuiteKey];
70
+ BOOL consumed = [defaults boolForKey:kConsumedKey];
71
+
72
+ NSLog(@"[DeferredLink] consumed flag = %@, suite = %@", consumed ? @"YES" : @"NO", kStorageSuiteKey);
73
+
74
+ if (consumed) {
75
+ NSString *lastValue = [defaults objectForKey:kLastValueKey];
76
+ NSLog(@"[DeferredLink] already consumed (lastValue: %@), returning not-found", lastValue);
77
+ resolve([self buildNotFoundResult]);
78
+ return;
79
+ }
80
+
81
+ // UIPasteboard MUST be accessed on the main thread (iOS 16+ silently
82
+ // returns nil and skips the paste banner when accessed off-main).
83
+ dispatch_async(dispatch_get_main_queue(), ^{
84
+ NSLog(@"[DeferredLink] reading pasteboard on main thread");
85
+
86
+ NSDictionary *payload = [self parsePasteboardPayload];
87
+
88
+ if (!payload) {
89
+ NSLog(@"[DeferredLink] pasteboard payload is nil — returning not-found");
90
+ resolve([self buildNotFoundResult]);
91
+ return;
92
+ }
93
+
94
+ NSString *url = payload[@"url"];
95
+ NSNumber *clickedAt = payload[@"clickedAt"];
96
+
97
+ NSLog(@"[DeferredLink] payload url=%@, clickedAt=%@, rawValue=%@",
98
+ url, clickedAt, payload[@"rawValue"]);
99
+
100
+ // TTL enforcement: reject expired payloads
101
+ if (clickedAt && self->_configPasteboardTTLSeconds > 0) {
102
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
103
+ NSTimeInterval elapsed = now - [clickedAt doubleValue];
104
+ NSLog(@"[DeferredLink] TTL check: elapsed=%.0fs, limit=%.0fs",
105
+ elapsed, self->_configPasteboardTTLSeconds);
106
+ if (elapsed > self->_configPasteboardTTLSeconds) {
107
+ NSLog(@"[DeferredLink] payload expired — returning not-found");
108
+ resolve([self buildNotFoundResult]);
109
+ return;
110
+ }
111
+ }
112
+
113
+ // Domain validation
114
+ if (![self isDomainAllowed:url]) {
115
+ NSLog(@"[DeferredLink] domain not allowed for url: %@", url);
116
+ resolve([self buildNotFoundResult]);
117
+ return;
118
+ }
119
+
120
+ [self markConsumed:url];
121
+
122
+ NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
123
+ @"found": @YES,
124
+ @"source": @"ios_pasteboard",
125
+ @"url": url,
126
+ @"rawValue": payload[@"rawValue"] ?: url,
127
+ }];
128
+
129
+ if (clickedAt) {
130
+ result[@"clickedAt"] = clickedAt;
131
+ }
132
+
133
+ NSDictionary *metadata = [self parseUrlMetadata:url];
134
+ if (metadata && metadata.count > 0) {
135
+ result[@"metadata"] = metadata;
136
+ }
137
+
138
+ NSLog(@"[DeferredLink] SUCCESS — found deferred link: %@", url);
139
+ resolve([result copy]);
140
+ });
141
+ }
142
+
143
+ // clearConsumedDeferredLink — RCT_EXPORT_METHOD provides both bridge discovery and TurboModule compatibility
144
+ RCT_EXPORT_METHOD(clearConsumedDeferredLink:(RCTPromiseResolveBlock)resolve
145
+ reject:(RCTPromiseRejectBlock)reject)
146
+ {
147
+ NSLog(@"[DeferredLink] clearConsumedDeferredLink called");
148
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kStorageSuiteKey];
149
+ [defaults removeObjectForKey:kConsumedKey];
150
+ [defaults removeObjectForKey:kLastValueKey];
151
+ [defaults removeObjectForKey:kLastConsumedAtKey];
152
+ resolve(nil);
153
+ }
154
+
155
+ #pragma mark - Pasteboard
156
+
157
+ /// Reads and parses pasteboard content.
158
+ /// Payload format: `<prefix><epoch_seconds>|<url>` (with timestamp)
159
+ /// or: `<prefix><url>` (legacy, no TTL enforcement)
160
+ /// Returns dictionary with keys: url, rawValue, clickedAt (optional), or nil if invalid.
161
+ - (NSDictionary *)parsePasteboardPayload {
162
+ UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
163
+ NSString *text = pasteboard.string;
164
+
165
+ NSLog(@"[DeferredLink] parsePasteboardPayload — isMainThread=%@, raw text=%@",
166
+ [NSThread isMainThread] ? @"YES" : @"NO",
167
+ text ? [NSString stringWithFormat:@"\"%@\" (len=%lu)", text, (unsigned long)text.length] : @"nil");
168
+
169
+ if (!text || text.length == 0) {
170
+ NSLog(@"[DeferredLink] pasteboard is empty");
171
+ return nil;
172
+ }
173
+
174
+ // Prefix filtering
175
+ if (_configPasteboardPrefix && _configPasteboardPrefix.length > 0) {
176
+ if (![text hasPrefix:_configPasteboardPrefix]) {
177
+ return nil;
178
+ }
179
+ NSString *rawValue = text;
180
+ text = [text substringFromIndex:_configPasteboardPrefix.length];
181
+
182
+ if (text.length == 0) {
183
+ return nil;
184
+ }
185
+
186
+ // Try to parse timestamp|url format
187
+ NSRange separatorRange = [text rangeOfString:kPayloadSeparator];
188
+ if (separatorRange.location != NSNotFound) {
189
+ NSString *timestampStr = [text substringToIndex:separatorRange.location];
190
+ NSString *url = [text substringFromIndex:NSMaxRange(separatorRange)];
191
+
192
+ // Validate timestamp is numeric
193
+ NSTimeInterval timestamp = [timestampStr doubleValue];
194
+ if (timestamp > 0 && url.length > 0) {
195
+ return @{
196
+ @"url": url,
197
+ @"rawValue": rawValue,
198
+ @"clickedAt": @(timestamp),
199
+ };
200
+ }
201
+ }
202
+
203
+ // Legacy format: entire remaining text is the URL
204
+ return @{
205
+ @"url": text,
206
+ @"rawValue": rawValue,
207
+ };
208
+ }
209
+
210
+ // No prefix configured: treat entire text as URL
211
+ return @{
212
+ @"url": text,
213
+ @"rawValue": text,
214
+ };
215
+ }
216
+
217
+ #pragma mark - Validation
218
+
219
+ - (BOOL)isDomainAllowed:(NSString *)urlString {
220
+ if (_configDomains.count == 0) {
221
+ return YES;
222
+ }
223
+
224
+ NSURL *url = [NSURL URLWithString:urlString];
225
+ NSString *host = url.host;
226
+ if (!host) {
227
+ return NO;
228
+ }
229
+
230
+ for (NSString *domain in _configDomains) {
231
+ if ([host isEqualToString:domain] ||
232
+ [host hasSuffix:[@"." stringByAppendingString:domain]]) {
233
+ return YES;
234
+ }
235
+ }
236
+
237
+ return NO;
238
+ }
239
+
240
+ #pragma mark - URL Metadata
241
+
242
+ - (NSDictionary *)parseUrlMetadata:(NSString *)urlString {
243
+ NSURL *url = [NSURL URLWithString:urlString];
244
+ if (!url) {
245
+ return nil;
246
+ }
247
+
248
+ NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
249
+ NSArray<NSURLQueryItem *> *queryItems = components.queryItems;
250
+ if (!queryItems || queryItems.count == 0) {
251
+ return nil;
252
+ }
253
+
254
+ NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
255
+ for (NSURLQueryItem *item in queryItems) {
256
+ if (item.value) {
257
+ metadata[item.name] = item.value;
258
+ }
259
+ }
260
+
261
+ return metadata.count > 0 ? [metadata copy] : nil;
262
+ }
263
+
264
+ #pragma mark - Storage
265
+
266
+ - (void)markConsumed:(NSString *)url {
267
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kStorageSuiteKey];
268
+ [defaults setBool:YES forKey:kConsumedKey];
269
+ [defaults setObject:url forKey:kLastValueKey];
270
+ [defaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kLastConsumedAtKey];
271
+ }
272
+
273
+ - (NSDictionary *)buildNotFoundResult {
274
+ return @{
275
+ @"found": @NO,
276
+ @"source": @"none",
277
+ };
278
+ }
279
+
280
+ #pragma mark - TurboModule
281
+
282
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
283
+ (const facebook::react::ObjCTurboModule::InitParams &)params
284
+ {
285
+ return std::make_shared<facebook::react::NativeDeferredLinkSpecJSI>(params);
286
+ }
287
+
288
+ @end
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.get('DeferredLink');
5
+ //# sourceMappingURL=NativeDeferredLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeDeferredLink.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAQlD,eAAeA,mBAAmB,CAACC,GAAG,CAAO,cAAc,CAAC","ignoreList":[]}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ import { NativeModules } from 'react-native';
4
+ import NativeDeferredLink from "./NativeDeferredLink.js";
5
+ const TAG = '[react-native-deferred-link]';
6
+ const LINKING_WARNING = `${TAG} Native module not found. ` + 'Make sure you have run `pod install` and rebuilt the app. ' + 'Deferred link features will be unavailable.';
7
+ const nativeModule = NativeDeferredLink ?? NativeModules.DeferredLink ?? null;
8
+ if (!nativeModule) {
9
+ console.warn(LINKING_WARNING);
10
+ } else {
11
+ console.log(`${TAG} Native module loaded (turbo=${!!NativeDeferredLink}, bridge=${!!NativeModules.DeferredLink})`);
12
+ }
13
+ const NOT_FOUND_RESULT = {
14
+ found: false,
15
+ source: 'none'
16
+ };
17
+ export const DeferredLink = {
18
+ configure(config) {
19
+ if (!nativeModule) {
20
+ console.warn(`${TAG} configure skipped — native module is null`);
21
+ return;
22
+ }
23
+ console.log(`${TAG} configure`, JSON.stringify(config));
24
+ nativeModule.configure(config);
25
+ },
26
+ async getInitialDeferredLink() {
27
+ if (!nativeModule) {
28
+ console.warn(`${TAG} getInitialDeferredLink skipped — native module is null`);
29
+ return NOT_FOUND_RESULT;
30
+ }
31
+ console.log(`${TAG} getInitialDeferredLink called`);
32
+ const raw = await nativeModule.getInitialDeferredLink();
33
+ const result = raw;
34
+ console.log(`${TAG} getInitialDeferredLink result:`, JSON.stringify(result));
35
+ return result;
36
+ },
37
+ async clearConsumedDeferredLink() {
38
+ if (!nativeModule) {
39
+ return;
40
+ }
41
+ console.log(`${TAG} clearConsumedDeferredLink called`);
42
+ await nativeModule.clearConsumedDeferredLink();
43
+ }
44
+ };
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NativeModules","NativeDeferredLink","TAG","LINKING_WARNING","nativeModule","DeferredLink","console","warn","log","NOT_FOUND_RESULT","found","source","configure","config","JSON","stringify","getInitialDeferredLink","raw","result","clearConsumedDeferredLink"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,aAAa,QAAQ,cAAc;AAG5C,OAAOC,kBAAkB,MAAM,yBAAsB;AAcrD,MAAMC,GAAG,GAAG,8BAA8B;AAE1C,MAAMC,eAAe,GACnB,GAAGD,GAAG,4BAA4B,GAClC,4DAA4D,GAC5D,6CAA6C;AAE/C,MAAME,YAAyB,GAC7BH,kBAAkB,IACjBD,aAAa,CAACK,YAAiC,IAChD,IAAI;AAEN,IAAI,CAACD,YAAY,EAAE;EACjBE,OAAO,CAACC,IAAI,CAACJ,eAAe,CAAC;AAC/B,CAAC,MAAM;EACLG,OAAO,CAACE,GAAG,CACT,GAAGN,GAAG,gCAAgC,CAAC,CAACD,kBAAkB,YAAY,CAAC,CAACD,aAAa,CAACK,YAAY,GACpG,CAAC;AACH;AAEA,MAAMI,gBAAoC,GAAG;EAC3CC,KAAK,EAAE,KAAK;EACZC,MAAM,EAAE;AACV,CAAC;AAED,OAAO,MAAMN,YAAiC,GAAG;EAC/CO,SAASA,CAACC,MAA0B,EAAQ;IAC1C,IAAI,CAACT,YAAY,EAAE;MACjBE,OAAO,CAACC,IAAI,CAAC,GAAGL,GAAG,4CAA4C,CAAC;MAChE;IACF;IACAI,OAAO,CAACE,GAAG,CAAC,GAAGN,GAAG,YAAY,EAAEY,IAAI,CAACC,SAAS,CAACF,MAAM,CAAC,CAAC;IACvDT,YAAY,CAACQ,SAAS,CAACC,MAA2B,CAAC;EACrD,CAAC;EAED,MAAMG,sBAAsBA,CAAA,EAAgC;IAC1D,IAAI,CAACZ,YAAY,EAAE;MACjBE,OAAO,CAACC,IAAI,CACV,GAAGL,GAAG,yDACR,CAAC;MACD,OAAOO,gBAAgB;IACzB;IACAH,OAAO,CAACE,GAAG,CAAC,GAAGN,GAAG,gCAAgC,CAAC;IACnD,MAAMe,GAAG,GAAG,MAAMb,YAAY,CAACY,sBAAsB,CAAC,CAAC;IACvD,MAAME,MAAM,GAAGD,GAAoC;IACnDX,OAAO,CAACE,GAAG,CACT,GAAGN,GAAG,iCAAiC,EACvCY,IAAI,CAACC,SAAS,CAACG,MAAM,CACvB,CAAC;IACD,OAAOA,MAAM;EACf,CAAC;EAED,MAAMC,yBAAyBA,CAAA,EAAkB;IAC/C,IAAI,CAACf,YAAY,EAAE;MACjB;IACF;IACAE,OAAO,CAACE,GAAG,CAAC,GAAGN,GAAG,mCAAmC,CAAC;IACtD,MAAME,YAAY,CAACe,yBAAyB,CAAC,CAAC;EAChD;AACF,CAAC","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,9 @@
1
+ import type { TurboModule } from 'react-native';
2
+ export interface Spec extends TurboModule {
3
+ configure(config: Object): void;
4
+ getInitialDeferredLink(): Promise<Object>;
5
+ clearConsumedDeferredLink(): Promise<void>;
6
+ }
7
+ declare const _default: Spec | null;
8
+ export default _default;
9
+ //# sourceMappingURL=NativeDeferredLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeDeferredLink.d.ts","sourceRoot":"","sources":["../../../src/NativeDeferredLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;;AAED,wBAA6D"}
@@ -0,0 +1,4 @@
1
+ import type { IDeferredLinkModule } from './types';
2
+ export type { DeferredLinkConfig, DeferredLinkResult, DeferredLinkSource, IDeferredLinkModule, } from './types';
3
+ export declare const DeferredLink: IDeferredLinkModule;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,SAAS,CAAC;AA2BjB,eAAO,MAAM,YAAY,EAAE,mBAkC1B,CAAC"}
@@ -0,0 +1,27 @@
1
+ export type DeferredLinkSource = 'android_install_referrer' | 'ios_pasteboard' | 'none';
2
+ export type DeferredLinkResult = {
3
+ found: boolean;
4
+ source: DeferredLinkSource;
5
+ url?: string;
6
+ rawValue?: string;
7
+ clickedAt?: number;
8
+ isFirstLaunch?: boolean;
9
+ metadata?: Record<string, string>;
10
+ };
11
+ export type DeferredLinkConfig = {
12
+ domains: string[];
13
+ appScheme?: string;
14
+ ios?: {
15
+ pasteboardPrefix?: string;
16
+ pasteboardTTLSeconds?: number;
17
+ };
18
+ android?: {
19
+ installReferrerParamKey?: string;
20
+ };
21
+ };
22
+ export interface IDeferredLinkModule {
23
+ configure(config: DeferredLinkConfig): void;
24
+ getInitialDeferredLink(): Promise<DeferredLinkResult>;
25
+ clearConsumedDeferredLink(): Promise<void>;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,0BAA0B,GAC1B,gBAAgB,GAChB,MAAM,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;IACF,OAAO,CAAC,EAAE;QACR,uBAAuB,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC5C,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACtD,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
package/package.json ADDED
@@ -0,0 +1,123 @@
1
+ {
2
+ "name": "@bdmakers/react-native-deferred-link",
3
+ "version": "1.0.0",
4
+ "description": "Deferred deep link recovery for React Native",
5
+ "main": "./lib/module/index.js",
6
+ "module": "./lib/module/index.js",
7
+ "types": "./lib/typescript/src/index.d.ts",
8
+ "react-native": "./src/index.tsx",
9
+ "source": "./src/index.tsx",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "android",
14
+ "ios",
15
+ "cpp",
16
+ "*.podspec",
17
+ "!ios/build",
18
+ "!android/build",
19
+ "!android/gradle",
20
+ "!android/gradlew",
21
+ "!android/gradlew.bat",
22
+ "!android/local.properties",
23
+ "!**/__tests__",
24
+ "!**/__fixtures__",
25
+ "!**/__mocks__",
26
+ "!**/.*"
27
+ ],
28
+ "scripts": {
29
+ "example": "yarn --cwd example",
30
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
31
+ "prepare": "bob build",
32
+ "typecheck": "tsc",
33
+ "lint": "eslint \"**/*.{js,ts,tsx}\""
34
+ },
35
+ "keywords": [
36
+ "react-native",
37
+ "ios",
38
+ "android",
39
+ "deep-link",
40
+ "deferred-link"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/bd-makers/react-native-deferred-link.git"
45
+ },
46
+ "author": "aijinet <dev@aijinet.com> (https://github.com/aijinet)",
47
+ "license": "MIT",
48
+ "bugs": {
49
+ "url": "https://github.com/bd-makers/react-native-deferred-link/issues"
50
+ },
51
+ "homepage": "https://github.com/bd-makers/react-native-deferred-link#readme",
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/",
54
+ "access": "public"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/compat": "^1.3.2",
58
+ "@eslint/eslintrc": "^3.3.1",
59
+ "@eslint/js": "^9.35.0",
60
+ "@react-native/babel-preset": "0.79.6",
61
+ "@react-native/eslint-config": "0.83.0",
62
+ "@types/react": "^19.0.0",
63
+ "del-cli": "^6.0.0",
64
+ "eslint": "^9.35.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "eslint-plugin-prettier": "^5.5.4",
67
+ "prettier": "^2.8.8",
68
+ "react": "19.0.0",
69
+ "react-native": "0.79.6",
70
+ "react-native-builder-bob": "^0.40.13",
71
+ "typescript": "^5.9.2"
72
+ },
73
+ "peerDependencies": {
74
+ "react": "*",
75
+ "react-native": "*"
76
+ },
77
+ "react-native-builder-bob": {
78
+ "source": "src",
79
+ "output": "lib",
80
+ "targets": [
81
+ [
82
+ "module",
83
+ {
84
+ "esm": true
85
+ }
86
+ ],
87
+ [
88
+ "typescript",
89
+ {
90
+ "project": "tsconfig.build.json"
91
+ }
92
+ ]
93
+ ]
94
+ },
95
+ "codegenConfig": {
96
+ "name": "DeferredLinkSpec",
97
+ "type": "modules",
98
+ "jsSrcsDir": "src",
99
+ "android": {
100
+ "javaPackageName": "com.deferredlink"
101
+ },
102
+ "ios": {
103
+ "modulesProvider": {
104
+ "DeferredLink": "DeferredLink"
105
+ }
106
+ }
107
+ },
108
+ "prettier": {
109
+ "quoteProps": "consistent",
110
+ "singleQuote": true,
111
+ "tabWidth": 2,
112
+ "trailingComma": "es5",
113
+ "useTabs": false
114
+ },
115
+ "create-react-native-library": {
116
+ "type": "turbo-module",
117
+ "languages": "kotlin-objc",
118
+ "tools": [
119
+ "eslint"
120
+ ],
121
+ "version": "0.57.2"
122
+ }
123
+ }
@@ -0,0 +1,10 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ export interface Spec extends TurboModule {
5
+ configure(config: Object): void;
6
+ getInitialDeferredLink(): Promise<Object>;
7
+ clearConsumedDeferredLink(): Promise<void>;
8
+ }
9
+
10
+ export default TurboModuleRegistry.get<Spec>('DeferredLink');
package/src/index.tsx ADDED
@@ -0,0 +1,77 @@
1
+ import { NativeModules } from 'react-native';
2
+
3
+ import type { Spec } from './NativeDeferredLink';
4
+ import NativeDeferredLink from './NativeDeferredLink';
5
+ import type {
6
+ DeferredLinkConfig,
7
+ DeferredLinkResult,
8
+ IDeferredLinkModule,
9
+ } from './types';
10
+
11
+ export type {
12
+ DeferredLinkConfig,
13
+ DeferredLinkResult,
14
+ DeferredLinkSource,
15
+ IDeferredLinkModule,
16
+ } from './types';
17
+
18
+ const TAG = '[react-native-deferred-link]';
19
+
20
+ const LINKING_WARNING =
21
+ `${TAG} Native module not found. ` +
22
+ 'Make sure you have run `pod install` and rebuilt the app. ' +
23
+ 'Deferred link features will be unavailable.';
24
+
25
+ const nativeModule: Spec | null =
26
+ NativeDeferredLink ??
27
+ (NativeModules.DeferredLink as Spec | undefined) ??
28
+ null;
29
+
30
+ if (!nativeModule) {
31
+ console.warn(LINKING_WARNING);
32
+ } else {
33
+ console.log(
34
+ `${TAG} Native module loaded (turbo=${!!NativeDeferredLink}, bridge=${!!NativeModules.DeferredLink})`
35
+ );
36
+ }
37
+
38
+ const NOT_FOUND_RESULT: DeferredLinkResult = {
39
+ found: false,
40
+ source: 'none',
41
+ };
42
+
43
+ export const DeferredLink: IDeferredLinkModule = {
44
+ configure(config: DeferredLinkConfig): void {
45
+ if (!nativeModule) {
46
+ console.warn(`${TAG} configure skipped — native module is null`);
47
+ return;
48
+ }
49
+ console.log(`${TAG} configure`, JSON.stringify(config));
50
+ nativeModule.configure(config as unknown as Object);
51
+ },
52
+
53
+ async getInitialDeferredLink(): Promise<DeferredLinkResult> {
54
+ if (!nativeModule) {
55
+ console.warn(
56
+ `${TAG} getInitialDeferredLink skipped — native module is null`
57
+ );
58
+ return NOT_FOUND_RESULT;
59
+ }
60
+ console.log(`${TAG} getInitialDeferredLink called`);
61
+ const raw = await nativeModule.getInitialDeferredLink();
62
+ const result = raw as unknown as DeferredLinkResult;
63
+ console.log(
64
+ `${TAG} getInitialDeferredLink result:`,
65
+ JSON.stringify(result)
66
+ );
67
+ return result;
68
+ },
69
+
70
+ async clearConsumedDeferredLink(): Promise<void> {
71
+ if (!nativeModule) {
72
+ return;
73
+ }
74
+ console.log(`${TAG} clearConsumedDeferredLink called`);
75
+ await nativeModule.clearConsumedDeferredLink();
76
+ },
77
+ };
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ export type DeferredLinkSource =
2
+ | 'android_install_referrer'
3
+ | 'ios_pasteboard'
4
+ | 'none';
5
+
6
+ export type DeferredLinkResult = {
7
+ found: boolean;
8
+ source: DeferredLinkSource;
9
+ url?: string;
10
+ rawValue?: string;
11
+ clickedAt?: number;
12
+ isFirstLaunch?: boolean;
13
+ metadata?: Record<string, string>;
14
+ };
15
+
16
+ export type DeferredLinkConfig = {
17
+ domains: string[];
18
+ appScheme?: string;
19
+ ios?: {
20
+ pasteboardPrefix?: string;
21
+ pasteboardTTLSeconds?: number;
22
+ };
23
+ android?: {
24
+ installReferrerParamKey?: string;
25
+ };
26
+ };
27
+
28
+ export interface IDeferredLinkModule {
29
+ configure(config: DeferredLinkConfig): void;
30
+ getInitialDeferredLink(): Promise<DeferredLinkResult>;
31
+ clearConsumedDeferredLink(): Promise<void>;
32
+ }