@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.
- package/DeferredLink.podspec +20 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/android/build.gradle +71 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/deferredlink/DeferredLinkModule.kt +134 -0
- package/android/src/main/java/com/deferredlink/DeferredLinkPackage.kt +31 -0
- package/android/src/main/java/com/deferredlink/DeferredLinkStorage.kt +45 -0
- package/android/src/main/java/com/deferredlink/ReferrerParser.kt +77 -0
- package/ios/DeferredLink.h +4 -0
- package/ios/DeferredLink.mm +288 -0
- package/lib/module/NativeDeferredLink.js +5 -0
- package/lib/module/NativeDeferredLink.js.map +1 -0
- package/lib/module/index.js +45 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeDeferredLink.d.ts +9 -0
- package/lib/typescript/src/NativeDeferredLink.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +27 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +123 -0
- package/src/NativeDeferredLink.ts +10 -0
- package/src/index.tsx +77 -0
- package/src/types.ts +32 -0
|
@@ -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,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,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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|