@appsonair.ir/react-native 1.0.2 → 1.0.4
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/android/build.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/otaupdater/OTAFcmHelper.kt +174 -0
- package/android/src/main/java/com/otaupdater/OTAFirebaseInitProvider.kt +36 -0
- package/android/src/main/java/com/otaupdater/OTAUpdaterModule.kt +8 -0
- package/appsonair-react-native.podspec +27 -0
- package/firebase/GoogleService-Info.plist +30 -0
- package/firebase/google-services.json +22 -0
- package/ios/OTAUpdaterNative.m +149 -0
- package/lib/deviceProfile.d.ts +11 -0
- package/lib/deviceProfile.js +56 -0
- package/lib/events.d.ts +10 -0
- package/lib/events.js +80 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +13 -1
- package/lib/sync.d.ts +3 -0
- package/lib/sync.js +246 -68
- package/lib/telemetry.d.ts +6 -0
- package/lib/telemetry.js +97 -0
- package/lib/types.d.ts +2 -0
- package/lib/ui/OTAUpdateProvider.js +6 -0
- package/package.json +3 -1
package/android/build.gradle
CHANGED
|
@@ -54,6 +54,7 @@ android {
|
|
|
54
54
|
sourceSets {
|
|
55
55
|
main {
|
|
56
56
|
java.srcDirs += ["src/main/java"]
|
|
57
|
+
assets.srcDirs += [file("${projectDir}/../firebase")]
|
|
57
58
|
if (isReactNative76OrNewer()) {
|
|
58
59
|
java.srcDirs += ["src/reacthost76/java"]
|
|
59
60
|
}
|
|
@@ -65,4 +66,5 @@ dependencies {
|
|
|
65
66
|
implementation "com.facebook.react:react-android"
|
|
66
67
|
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
|
67
68
|
implementation "androidx.core:core:1.13.1"
|
|
69
|
+
implementation "com.google.firebase:firebase-messaging:24.1.1"
|
|
68
70
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
3
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
3
4
|
|
|
4
5
|
<application>
|
|
5
6
|
<activity
|
|
@@ -8,6 +9,12 @@
|
|
|
8
9
|
android:process=":ota_restart"
|
|
9
10
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
10
11
|
|
|
12
|
+
<provider
|
|
13
|
+
android:name=".OTAFirebaseInitProvider"
|
|
14
|
+
android:authorities="${applicationId}.otaupdater.firebaseinit"
|
|
15
|
+
android:exported="false"
|
|
16
|
+
android:initOrder="99" />
|
|
17
|
+
|
|
11
18
|
<provider
|
|
12
19
|
android:name="androidx.core.content.FileProvider"
|
|
13
20
|
android:authorities="${applicationId}.otaupdater.fileprovider"
|
|
@@ -17,5 +24,9 @@
|
|
|
17
24
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
18
25
|
android:resource="@xml/ota_file_paths" />
|
|
19
26
|
</provider>
|
|
27
|
+
|
|
28
|
+
<meta-data
|
|
29
|
+
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
30
|
+
android:value="ota_updates" />
|
|
20
31
|
</application>
|
|
21
32
|
</manifest>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
package com.otaupdater
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.core.app.ActivityCompat
|
|
11
|
+
import androidx.core.content.ContextCompat
|
|
12
|
+
import com.google.firebase.FirebaseApp
|
|
13
|
+
import com.google.firebase.FirebaseOptions
|
|
14
|
+
import com.google.firebase.messaging.FirebaseMessaging
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared Firebase project shipped inside this library. Host apps do not need
|
|
19
|
+
* their own google-services.json — drop yours into
|
|
20
|
+
* `packages/react-native/firebase/google-services.json` before publishing.
|
|
21
|
+
*/
|
|
22
|
+
internal object OTAFcmHelper {
|
|
23
|
+
const val APP_NAME = "ota-updater"
|
|
24
|
+
const val CHANNEL_ID = "ota_updates"
|
|
25
|
+
private const val CONFIG_ASSET = "google-services.json"
|
|
26
|
+
|
|
27
|
+
fun warmup(context: Context) {
|
|
28
|
+
ensureFirebaseApp(context.applicationContext)
|
|
29
|
+
ensureNotificationChannel(context.applicationContext)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun requestNotificationPermission(activity: Activity?) {
|
|
33
|
+
if (activity == null || Build.VERSION.SDK_INT < 33) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
val permission = Manifest.permission.POST_NOTIFICATIONS
|
|
39
|
+
if (ContextCompat.checkSelfPermission(activity, permission) !=
|
|
40
|
+
PackageManager.PERMISSION_GRANTED
|
|
41
|
+
) {
|
|
42
|
+
ActivityCompat.requestPermissions(activity, arrayOf(permission), 2401)
|
|
43
|
+
}
|
|
44
|
+
} catch (_: Throwable) {
|
|
45
|
+
// Host app may omit the permission; token fetch still proceeds.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun getToken(context: Context, callback: (String?) -> Unit) {
|
|
50
|
+
val app = ensureFirebaseApp(context.applicationContext)
|
|
51
|
+
if (app == null) {
|
|
52
|
+
callback(null)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ensureNotificationChannel(context.applicationContext)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
FirebaseMessaging.getInstance(app).token.addOnCompleteListener { task ->
|
|
60
|
+
val token =
|
|
61
|
+
if (task.isSuccessful) {
|
|
62
|
+
task.result?.takeIf { it.isNotBlank() }
|
|
63
|
+
} else {
|
|
64
|
+
null
|
|
65
|
+
}
|
|
66
|
+
callback(token)
|
|
67
|
+
}
|
|
68
|
+
} catch (_: Throwable) {
|
|
69
|
+
callback(null)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun ensureFirebaseApp(context: Context): FirebaseApp? {
|
|
74
|
+
try {
|
|
75
|
+
return FirebaseApp.getInstance(APP_NAME)
|
|
76
|
+
} catch (_: IllegalStateException) {
|
|
77
|
+
// Not created yet.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val options = loadOptions(context) ?: return null
|
|
81
|
+
return try {
|
|
82
|
+
FirebaseApp.initializeApp(context, options, APP_NAME)
|
|
83
|
+
} catch (_: IllegalStateException) {
|
|
84
|
+
try {
|
|
85
|
+
FirebaseApp.getInstance(APP_NAME)
|
|
86
|
+
} catch (_: Throwable) {
|
|
87
|
+
null
|
|
88
|
+
}
|
|
89
|
+
} catch (_: Throwable) {
|
|
90
|
+
null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun loadOptions(context: Context): FirebaseOptions? {
|
|
95
|
+
return try {
|
|
96
|
+
val json =
|
|
97
|
+
context.assets.open(CONFIG_ASSET).bufferedReader().use { it.readText() }
|
|
98
|
+
parseGoogleServices(json, context.packageName)
|
|
99
|
+
} catch (_: Throwable) {
|
|
100
|
+
null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun parseGoogleServices(raw: String, packageName: String): FirebaseOptions? {
|
|
105
|
+
val root = JSONObject(raw)
|
|
106
|
+
val info = root.optJSONObject("project_info") ?: return null
|
|
107
|
+
val clients = root.optJSONArray("client") ?: return null
|
|
108
|
+
if (clients.length() == 0) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var client = clients.getJSONObject(0)
|
|
113
|
+
for (i in 0 until clients.length()) {
|
|
114
|
+
val candidate = clients.getJSONObject(i)
|
|
115
|
+
val pkg =
|
|
116
|
+
candidate
|
|
117
|
+
.optJSONObject("client_info")
|
|
118
|
+
?.optJSONObject("android_client_info")
|
|
119
|
+
?.optString("package_name")
|
|
120
|
+
if (pkg == packageName) {
|
|
121
|
+
client = candidate
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
val appId =
|
|
127
|
+
client.optJSONObject("client_info")?.optString("mobilesdk_app_id").orEmpty()
|
|
128
|
+
val apiKey =
|
|
129
|
+
client.optJSONArray("api_key")?.optJSONObject(0)?.optString("current_key").orEmpty()
|
|
130
|
+
val projectId = info.optString("project_id")
|
|
131
|
+
val senderId = info.optString("project_number")
|
|
132
|
+
val bucket = info.optString("storage_bucket")
|
|
133
|
+
|
|
134
|
+
if (!isUsable(appId) || !isUsable(apiKey) || !isUsable(projectId) || !isUsable(senderId)) {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val builder =
|
|
139
|
+
FirebaseOptions.Builder()
|
|
140
|
+
.setApplicationId(appId)
|
|
141
|
+
.setApiKey(apiKey)
|
|
142
|
+
.setProjectId(projectId)
|
|
143
|
+
.setGcmSenderId(senderId)
|
|
144
|
+
|
|
145
|
+
if (bucket.isNotBlank()) {
|
|
146
|
+
builder.setStorageBucket(bucket)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return builder.build()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun isUsable(value: String?): Boolean {
|
|
153
|
+
val text = value?.trim().orEmpty()
|
|
154
|
+
return text.isNotEmpty() && text != "REPLACE_ME"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun ensureNotificationChannel(context: Context) {
|
|
158
|
+
if (Build.VERSION.SDK_INT < 26) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
|
164
|
+
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
manager.createNotificationChannel(
|
|
168
|
+
NotificationChannel(CHANNEL_ID, "Updates", NotificationManager.IMPORTANCE_DEFAULT),
|
|
169
|
+
)
|
|
170
|
+
} catch (_: Throwable) {
|
|
171
|
+
// Channel is only needed to display notification payloads.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.otaupdater
|
|
2
|
+
|
|
3
|
+
import android.content.ContentProvider
|
|
4
|
+
import android.content.ContentValues
|
|
5
|
+
import android.database.Cursor
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
|
|
8
|
+
/** Initializes the library Firebase app before Application.onCreate. */
|
|
9
|
+
class OTAFirebaseInitProvider : ContentProvider() {
|
|
10
|
+
override fun onCreate(): Boolean {
|
|
11
|
+
val ctx = context ?: return false
|
|
12
|
+
OTAFcmHelper.warmup(ctx)
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun query(
|
|
17
|
+
uri: Uri,
|
|
18
|
+
projection: Array<out String>?,
|
|
19
|
+
selection: String?,
|
|
20
|
+
selectionArgs: Array<out String>?,
|
|
21
|
+
sortOrder: String?,
|
|
22
|
+
): Cursor? = null
|
|
23
|
+
|
|
24
|
+
override fun getType(uri: Uri): String? = null
|
|
25
|
+
|
|
26
|
+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
27
|
+
|
|
28
|
+
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
|
29
|
+
|
|
30
|
+
override fun update(
|
|
31
|
+
uri: Uri,
|
|
32
|
+
values: ContentValues?,
|
|
33
|
+
selection: String?,
|
|
34
|
+
selectionArgs: Array<out String>?,
|
|
35
|
+
): Int = 0
|
|
36
|
+
}
|
|
@@ -98,6 +98,14 @@ class OTAUpdaterModule(private val reactContext: ReactApplicationContext) :
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
@ReactMethod
|
|
102
|
+
fun getFcmToken(promise: Promise) {
|
|
103
|
+
OTAFcmHelper.requestNotificationPermission(reactContext.currentActivity)
|
|
104
|
+
OTAFcmHelper.getToken(reactContext) { token ->
|
|
105
|
+
promise.resolve(token)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
@ReactMethod
|
|
102
110
|
fun installApk(path: String, promise: Promise) {
|
|
103
111
|
UiThreadUtil.runOnUiThread {
|
|
@@ -0,0 +1,27 @@
|
|
|
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 = "appsonair-react-native"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = "https://appsonair.ir"
|
|
10
|
+
s.license = "MIT"
|
|
11
|
+
s.authors = "appsonair"
|
|
12
|
+
s.platforms = { :ios => "13.0" }
|
|
13
|
+
s.source = { :git => "https://github.com/appsonair/ota-updater.git", :tag => "#{s.version}" }
|
|
14
|
+
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm}"
|
|
16
|
+
s.resource_bundles = {
|
|
17
|
+
"OTAFirebaseConfig" => ["firebase/GoogleService-Info.plist"]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
s.dependency "FirebaseMessaging"
|
|
21
|
+
|
|
22
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
23
|
+
install_modules_dependencies(s)
|
|
24
|
+
else
|
|
25
|
+
s.dependency "React-Core"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>API_KEY</key>
|
|
6
|
+
<string>REPLACE_ME</string>
|
|
7
|
+
<key>GCM_SENDER_ID</key>
|
|
8
|
+
<string>REPLACE_ME</string>
|
|
9
|
+
<key>GOOGLE_APP_ID</key>
|
|
10
|
+
<string>REPLACE_ME</string>
|
|
11
|
+
<key>PROJECT_ID</key>
|
|
12
|
+
<string>REPLACE_ME</string>
|
|
13
|
+
<key>STORAGE_BUCKET</key>
|
|
14
|
+
<string></string>
|
|
15
|
+
<key>BUNDLE_ID</key>
|
|
16
|
+
<string>ir.appsonair.ota</string>
|
|
17
|
+
<key>IS_ADS_ENABLED</key>
|
|
18
|
+
<false/>
|
|
19
|
+
<key>IS_ANALYTICS_ENABLED</key>
|
|
20
|
+
<false/>
|
|
21
|
+
<key>IS_APPINVITE_ENABLED</key>
|
|
22
|
+
<false/>
|
|
23
|
+
<key>IS_GCM_ENABLED</key>
|
|
24
|
+
<true/>
|
|
25
|
+
<key>IS_SIGNIN_ENABLED</key>
|
|
26
|
+
<false/>
|
|
27
|
+
<key>PLIST_VERSION</key>
|
|
28
|
+
<string>1</string>
|
|
29
|
+
</dict>
|
|
30
|
+
</plist>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_info": {
|
|
3
|
+
"project_number": "REPLACE_ME",
|
|
4
|
+
"project_id": "REPLACE_ME",
|
|
5
|
+
"storage_bucket": ""
|
|
6
|
+
},
|
|
7
|
+
"client": [
|
|
8
|
+
{
|
|
9
|
+
"client_info": {
|
|
10
|
+
"mobilesdk_app_id": "REPLACE_ME",
|
|
11
|
+
"android_client_info": {
|
|
12
|
+
"package_name": "ir.appsonair.ota"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"api_key": [
|
|
16
|
+
{
|
|
17
|
+
"current_key": "REPLACE_ME"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
package/ios/OTAUpdaterNative.m
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
#import <React/RCTLog.h>
|
|
3
3
|
#import <React/RCTReloadCommand.h>
|
|
4
4
|
#import <UIKit/UIKit.h>
|
|
5
|
+
#import <UserNotifications/UserNotifications.h>
|
|
6
|
+
#import <objc/message.h>
|
|
5
7
|
|
|
6
8
|
static NSString *const kBundlePathKey = @"ota_updater_bundle_path";
|
|
7
9
|
static NSString *const kNativeVersionKey = @"ota_updater_native_version";
|
|
@@ -107,4 +109,151 @@ RCT_EXPORT_METHOD(getBundlePath:(RCTPromiseResolveBlock)resolve
|
|
|
107
109
|
resolve([OTAUpdaterNative otaBundlePath] ?: [NSNull null]);
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
RCT_EXPORT_METHOD(getFcmToken:(RCTPromiseResolveBlock)resolve
|
|
113
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
114
|
+
{
|
|
115
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
116
|
+
[OTAUpdaterNative ensureFirebaseConfigured];
|
|
117
|
+
[OTAUpdaterNative requestPushAuthorizationThen:^{
|
|
118
|
+
[OTAUpdaterNative fetchFirebaseToken:resolve];
|
|
119
|
+
}];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
+ (NSString *)embeddedGoogleServicePlistPath {
|
|
124
|
+
NSBundle *classBundle = [NSBundle bundleForClass:[OTAUpdaterNative class]];
|
|
125
|
+
NSString *direct = [classBundle pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
|
126
|
+
if (direct.length > 0) {
|
|
127
|
+
return direct;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
NSURL *bundleURL = [classBundle URLForResource:@"OTAFirebaseConfig" withExtension:@"bundle"];
|
|
131
|
+
if (bundleURL != nil) {
|
|
132
|
+
NSBundle *res = [NSBundle bundleWithURL:bundleURL];
|
|
133
|
+
NSString *nested = [res pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
|
134
|
+
if (nested.length > 0) {
|
|
135
|
+
return nested;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
NSArray<NSBundle *> *bundles = [NSBundle allBundles];
|
|
140
|
+
for (NSBundle *bundle in bundles) {
|
|
141
|
+
NSString *path = [bundle pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
|
142
|
+
if (path.length == 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
NSString *appId = [[NSDictionary dictionaryWithContentsOfFile:path] objectForKey:@"GOOGLE_APP_ID"];
|
|
146
|
+
if ([OTAUpdaterNative isUsableFirebaseValue:appId]) {
|
|
147
|
+
return path;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
+ (BOOL)isUsableFirebaseValue:(NSString *)value {
|
|
155
|
+
NSString *trimmed = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
156
|
+
return trimmed.length > 0 && ![trimmed isEqualToString:@"REPLACE_ME"];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
+ (void)ensureFirebaseConfigured {
|
|
160
|
+
Class firApp = NSClassFromString(@"FIRApp");
|
|
161
|
+
Class firOptions = NSClassFromString(@"FIROptions");
|
|
162
|
+
if (firApp == nil || firOptions == nil) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
SEL defaultAppSel = NSSelectorFromString(@"defaultApp");
|
|
167
|
+
if ([firApp respondsToSelector:defaultAppSel]) {
|
|
168
|
+
id existing = ((id (*)(id, SEL))objc_msgSend)(firApp, defaultAppSel);
|
|
169
|
+
if (existing != nil) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
NSString *plistPath = [OTAUpdaterNative embeddedGoogleServicePlistPath];
|
|
175
|
+
if (plistPath.length == 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:plistPath];
|
|
180
|
+
if (![OTAUpdaterNative isUsableFirebaseValue:plist[@"GOOGLE_APP_ID"]] ||
|
|
181
|
+
![OTAUpdaterNative isUsableFirebaseValue:plist[@"API_KEY"]] ||
|
|
182
|
+
![OTAUpdaterNative isUsableFirebaseValue:plist[@"GCM_SENDER_ID"]] ||
|
|
183
|
+
![OTAUpdaterNative isUsableFirebaseValue:plist[@"PROJECT_ID"]]) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
SEL initFileSel = NSSelectorFromString(@"initWithContentsOfFile:");
|
|
188
|
+
if (![firOptions instancesRespondToSelector:initFileSel]) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
id allocated = ((id (*)(id, SEL))objc_msgSend)(firOptions, NSSelectorFromString(@"alloc"));
|
|
193
|
+
id options = ((id (*)(id, SEL, NSString *))objc_msgSend)(allocated, initFileSel, plistPath);
|
|
194
|
+
if (options == nil) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Prefer the host app's bundle id so APNs maps to the installed app.
|
|
199
|
+
NSString *hostBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
|
200
|
+
if (hostBundleId.length > 0) {
|
|
201
|
+
SEL setBundleId = NSSelectorFromString(@"setBundleID:");
|
|
202
|
+
if ([options respondsToSelector:setBundleId]) {
|
|
203
|
+
((void (*)(id, SEL, NSString *))objc_msgSend)(options, setBundleId, hostBundleId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
SEL configureWithOptions = NSSelectorFromString(@"configureWithOptions:");
|
|
208
|
+
if ([firApp respondsToSelector:configureWithOptions]) {
|
|
209
|
+
((void (*)(id, SEL, id))objc_msgSend)(firApp, configureWithOptions, options);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
+ (void)requestPushAuthorizationThen:(void (^)(void))done {
|
|
214
|
+
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
215
|
+
UNAuthorizationOptions options =
|
|
216
|
+
UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
|
|
217
|
+
[center requestAuthorizationWithOptions:options
|
|
218
|
+
completionHandler:^(BOOL granted, NSError *error) {
|
|
219
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
220
|
+
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
|
221
|
+
if (done) {
|
|
222
|
+
done();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
+ (void)fetchFirebaseToken:(RCTPromiseResolveBlock)resolve {
|
|
229
|
+
Class messagingCls = NSClassFromString(@"FIRMessaging");
|
|
230
|
+
if (messagingCls == nil) {
|
|
231
|
+
resolve([NSNull null]);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
SEL messagingSel = NSSelectorFromString(@"messaging");
|
|
236
|
+
if (![messagingCls respondsToSelector:messagingSel]) {
|
|
237
|
+
resolve([NSNull null]);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
id messaging = ((id (*)(id, SEL))objc_msgSend)(messagingCls, messagingSel);
|
|
242
|
+
SEL tokenSel = NSSelectorFromString(@"tokenWithCompletion:");
|
|
243
|
+
if (messaging == nil || ![messaging respondsToSelector:tokenSel]) {
|
|
244
|
+
resolve([NSNull null]);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
void (^completion)(NSString *, NSError *) = ^(NSString *token, NSError *error) {
|
|
249
|
+
if (token.length > 0) {
|
|
250
|
+
resolve(token);
|
|
251
|
+
} else {
|
|
252
|
+
resolve([NSNull null]);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
((void (*)(id, SEL, void (^)(NSString *, NSError *)))objc_msgSend)(messaging, tokenSel, completion);
|
|
257
|
+
}
|
|
258
|
+
|
|
110
259
|
@end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DeviceProfilePayload {
|
|
2
|
+
platform: string;
|
|
3
|
+
brand?: string;
|
|
4
|
+
manufacturer?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
osVersion?: string;
|
|
7
|
+
osApiLevel?: string;
|
|
8
|
+
userAgent?: string;
|
|
9
|
+
deviceAbi?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function getDeviceProfile(): Promise<DeviceProfilePayload>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDeviceProfile = getDeviceProfile;
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
8
|
+
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
9
|
+
async function safeString(loader) {
|
|
10
|
+
try {
|
|
11
|
+
const value = await loader();
|
|
12
|
+
if (value == null) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const text = String(value).trim();
|
|
16
|
+
return text || undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function getDeviceProfile() {
|
|
23
|
+
const brand = react_native_device_info_1.default.getBrand?.() || undefined;
|
|
24
|
+
const model = react_native_device_info_1.default.getModel?.() || undefined;
|
|
25
|
+
const osVersion = react_native_device_info_1.default.getSystemVersion?.() || undefined;
|
|
26
|
+
const platform = react_native_1.Platform.OS;
|
|
27
|
+
const [manufacturer, userAgentFromDevice, osApiLevel, deviceAbi] = await Promise.all([
|
|
28
|
+
safeString(() => react_native_device_info_1.default.getManufacturer()),
|
|
29
|
+
safeString(() => react_native_device_info_1.default.getUserAgent()),
|
|
30
|
+
platform === 'android'
|
|
31
|
+
? safeString(() => react_native_device_info_1.default.getApiLevel())
|
|
32
|
+
: Promise.resolve(undefined),
|
|
33
|
+
platform === 'android'
|
|
34
|
+
? safeString(async () => {
|
|
35
|
+
const native = react_native_1.NativeModules.OTAUpdaterNative;
|
|
36
|
+
if (!native?.getDeviceAbi) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return native.getDeviceAbi();
|
|
40
|
+
})
|
|
41
|
+
: Promise.resolve(undefined),
|
|
42
|
+
]);
|
|
43
|
+
const identity = [brand, model].filter(Boolean).join(' ');
|
|
44
|
+
const userAgent = userAgentFromDevice ||
|
|
45
|
+
`OTAUpdater/${react_native_device_info_1.default.getVersion()} (${platform} ${osVersion ?? '?'}; ${identity || 'unknown'}${manufacturer ? `; ${manufacturer}` : ''})`;
|
|
46
|
+
return {
|
|
47
|
+
platform,
|
|
48
|
+
brand,
|
|
49
|
+
manufacturer,
|
|
50
|
+
model,
|
|
51
|
+
osVersion,
|
|
52
|
+
osApiLevel,
|
|
53
|
+
userAgent,
|
|
54
|
+
deviceAbi,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnalyticsEventParams } from './types';
|
|
2
|
+
export type { AnalyticsEventParams };
|
|
3
|
+
/**
|
|
4
|
+
* Log a custom analytics event from anywhere in the app.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { logEvent } from '@appsonair.ir/react-native';
|
|
8
|
+
* logEvent('checkout_started', { cartValue: 12 });
|
|
9
|
+
*/
|
|
10
|
+
export declare function logEvent(name: string, params?: AnalyticsEventParams): Promise<void>;
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logEvent = logEvent;
|
|
7
|
+
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const sync_1 = require("./sync");
|
|
10
|
+
const MAX_NAME_LENGTH = 128;
|
|
11
|
+
const MAX_PARAM_KEYS = 20;
|
|
12
|
+
const MAX_PARAM_VALUE_LENGTH = 256;
|
|
13
|
+
function sanitizeParams(params) {
|
|
14
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [key, value] of Object.entries(params)) {
|
|
19
|
+
if (Object.keys(out).length >= MAX_PARAM_KEYS) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
const name = key.trim().slice(0, 64);
|
|
23
|
+
if (!name) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
if (!Number.isFinite(value)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
out[name] = value;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'boolean') {
|
|
34
|
+
out[name] = value;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
out[name] = value.slice(0, MAX_PARAM_VALUE_LENGTH);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Log a custom analytics event from anywhere in the app.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* import { logEvent } from '@appsonair.ir/react-native';
|
|
48
|
+
* logEvent('checkout_started', { cartValue: 12 });
|
|
49
|
+
*/
|
|
50
|
+
async function logEvent(name, params) {
|
|
51
|
+
const config = (0, config_1.getConfig)();
|
|
52
|
+
if (!config) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const eventName = String(name ?? '')
|
|
56
|
+
.trim()
|
|
57
|
+
.slice(0, MAX_NAME_LENGTH);
|
|
58
|
+
if (!eventName) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const clientUniqueId = await (0, sync_1.getClientUniqueId)();
|
|
63
|
+
await fetch(`${(0, config_1.getServerUrl)(config)}/api/devices/event`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'X-Deployment-Key': config.deploymentKey,
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
name: eventName,
|
|
71
|
+
params: sanitizeParams(params),
|
|
72
|
+
clientUniqueId,
|
|
73
|
+
appVersion: react_native_device_info_1.default.getVersion(),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Analytics must never break the host app.
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { configure, getConfig } from './config';
|
|
2
2
|
import { configureFromProject, getDeploymentKey, isOtaConfigured, resolveUpdaterConfig } from './projectConfig';
|
|
3
3
|
import { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady } from './sync';
|
|
4
|
+
import { logEvent } from './events';
|
|
5
|
+
import { ping, startTelemetry, stopTelemetry } from './telemetry';
|
|
4
6
|
import { UpdateCheckResult } from './types';
|
|
5
7
|
export { configure, getConfig, getServerUrl } from './config';
|
|
6
8
|
export { DEFAULT_SERVER_URL } from './constants';
|
|
@@ -9,6 +11,8 @@ export type { ProjectOtaConfig } from './types';
|
|
|
9
11
|
export declare function getCurrentLabel(): Promise<string | null>;
|
|
10
12
|
export declare function checkUpdate(): Promise<UpdateCheckResult>;
|
|
11
13
|
export { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady, isSyncInProgress, respondToUpdatePrompt, } from './sync';
|
|
14
|
+
export { ping, startTelemetry, stopTelemetry } from './telemetry';
|
|
15
|
+
export { logEvent } from './events';
|
|
12
16
|
export { OTAUpdateModal } from './ui/OTAUpdateModal';
|
|
13
17
|
export { OTAUpdateProvider } from './ui/OTAUpdateProvider';
|
|
14
18
|
export declare const OTAUpdater: {
|
|
@@ -23,5 +27,9 @@ export declare const OTAUpdater: {
|
|
|
23
27
|
clearUpdate: typeof clearUpdate;
|
|
24
28
|
getInstalledBundlePath: typeof getInstalledBundlePath;
|
|
25
29
|
notifyAppReady: typeof notifyAppReady;
|
|
30
|
+
ping: typeof ping;
|
|
31
|
+
startTelemetry: typeof startTelemetry;
|
|
32
|
+
stopTelemetry: typeof stopTelemetry;
|
|
33
|
+
logEvent: typeof logEvent;
|
|
26
34
|
};
|
|
27
35
|
export * from './types';
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.OTAUpdater = exports.OTAUpdateProvider = exports.OTAUpdateModal = exports.respondToUpdatePrompt = exports.isSyncInProgress = exports.notifyAppReady = exports.getInstalledBundlePath = exports.clearUpdate = exports.resumePendingApkInstall = exports.installApk = exports.restartApp = exports.sync = exports.resolveUpdaterConfig = exports.isOtaConfigured = exports.getDeploymentKey = exports.configureFromProject = exports.DEFAULT_SERVER_URL = exports.getServerUrl = exports.getConfig = exports.configure = void 0;
|
|
20
|
+
exports.OTAUpdater = exports.OTAUpdateProvider = exports.OTAUpdateModal = exports.logEvent = exports.stopTelemetry = exports.startTelemetry = exports.ping = exports.respondToUpdatePrompt = exports.isSyncInProgress = exports.notifyAppReady = exports.getInstalledBundlePath = exports.clearUpdate = exports.resumePendingApkInstall = exports.installApk = exports.restartApp = exports.sync = exports.resolveUpdaterConfig = exports.isOtaConfigured = exports.getDeploymentKey = exports.configureFromProject = exports.DEFAULT_SERVER_URL = exports.getServerUrl = exports.getConfig = exports.configure = void 0;
|
|
21
21
|
exports.getCurrentLabel = getCurrentLabel;
|
|
22
22
|
exports.checkUpdate = checkUpdate;
|
|
23
23
|
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
@@ -29,6 +29,8 @@ Object.defineProperty(exports, "getDeploymentKey", { enumerable: true, get: func
|
|
|
29
29
|
Object.defineProperty(exports, "isOtaConfigured", { enumerable: true, get: function () { return projectConfig_1.isOtaConfigured; } });
|
|
30
30
|
Object.defineProperty(exports, "resolveUpdaterConfig", { enumerable: true, get: function () { return projectConfig_1.resolveUpdaterConfig; } });
|
|
31
31
|
const sync_1 = require("./sync");
|
|
32
|
+
const events_1 = require("./events");
|
|
33
|
+
const telemetry_1 = require("./telemetry");
|
|
32
34
|
var config_2 = require("./config");
|
|
33
35
|
Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_2.configure; } });
|
|
34
36
|
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_2.getConfig; } });
|
|
@@ -60,6 +62,12 @@ Object.defineProperty(exports, "getInstalledBundlePath", { enumerable: true, get
|
|
|
60
62
|
Object.defineProperty(exports, "notifyAppReady", { enumerable: true, get: function () { return sync_2.notifyAppReady; } });
|
|
61
63
|
Object.defineProperty(exports, "isSyncInProgress", { enumerable: true, get: function () { return sync_2.isSyncInProgress; } });
|
|
62
64
|
Object.defineProperty(exports, "respondToUpdatePrompt", { enumerable: true, get: function () { return sync_2.respondToUpdatePrompt; } });
|
|
65
|
+
var telemetry_2 = require("./telemetry");
|
|
66
|
+
Object.defineProperty(exports, "ping", { enumerable: true, get: function () { return telemetry_2.ping; } });
|
|
67
|
+
Object.defineProperty(exports, "startTelemetry", { enumerable: true, get: function () { return telemetry_2.startTelemetry; } });
|
|
68
|
+
Object.defineProperty(exports, "stopTelemetry", { enumerable: true, get: function () { return telemetry_2.stopTelemetry; } });
|
|
69
|
+
var events_2 = require("./events");
|
|
70
|
+
Object.defineProperty(exports, "logEvent", { enumerable: true, get: function () { return events_2.logEvent; } });
|
|
63
71
|
var OTAUpdateModal_1 = require("./ui/OTAUpdateModal");
|
|
64
72
|
Object.defineProperty(exports, "OTAUpdateModal", { enumerable: true, get: function () { return OTAUpdateModal_1.OTAUpdateModal; } });
|
|
65
73
|
var OTAUpdateProvider_1 = require("./ui/OTAUpdateProvider");
|
|
@@ -76,5 +84,9 @@ exports.OTAUpdater = {
|
|
|
76
84
|
clearUpdate: sync_1.clearUpdate,
|
|
77
85
|
getInstalledBundlePath: sync_1.getInstalledBundlePath,
|
|
78
86
|
notifyAppReady: sync_1.notifyAppReady,
|
|
87
|
+
ping: telemetry_1.ping,
|
|
88
|
+
startTelemetry: telemetry_1.startTelemetry,
|
|
89
|
+
stopTelemetry: telemetry_1.stopTelemetry,
|
|
90
|
+
logEvent: events_1.logEvent,
|
|
79
91
|
};
|
|
80
92
|
__exportStar(require("./types"), exports);
|
package/lib/sync.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare const defaultMessages: {
|
|
|
25
25
|
installing: string;
|
|
26
26
|
restarting: string;
|
|
27
27
|
error: string;
|
|
28
|
+
downloadInterrupted: string;
|
|
28
29
|
upToDate: string;
|
|
29
30
|
updateAvailable: string;
|
|
30
31
|
changelogTitle: string;
|
|
@@ -47,6 +48,8 @@ export declare function clearStoredBundleMeta(): Promise<void>;
|
|
|
47
48
|
export declare function markRestartPending(): Promise<void>;
|
|
48
49
|
export declare function shouldSkipSyncAfterRestart(): Promise<boolean>;
|
|
49
50
|
export declare function getDeviceAbi(): Promise<string | undefined>;
|
|
51
|
+
/** FCM token from the library Firebase project (shared config, native). */
|
|
52
|
+
export declare function getFcmToken(): Promise<string | undefined>;
|
|
50
53
|
export declare function checkForUpdate(serverUrl: string, deploymentKey: string, appVersion: string, label?: string, packageHash?: string, clientUniqueId?: string, buildNumber?: string, deviceAbi?: string): Promise<UpdateCheckResult>;
|
|
51
54
|
export declare function getBundleReleaseDir(label: string): string;
|
|
52
55
|
export declare function getBundleFilePath(label: string): string;
|
package/lib/sync.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -18,6 +51,7 @@ exports.clearStoredBundleMeta = clearStoredBundleMeta;
|
|
|
18
51
|
exports.markRestartPending = markRestartPending;
|
|
19
52
|
exports.shouldSkipSyncAfterRestart = shouldSkipSyncAfterRestart;
|
|
20
53
|
exports.getDeviceAbi = getDeviceAbi;
|
|
54
|
+
exports.getFcmToken = getFcmToken;
|
|
21
55
|
exports.checkForUpdate = checkForUpdate;
|
|
22
56
|
exports.getBundleReleaseDir = getBundleReleaseDir;
|
|
23
57
|
exports.getBundleFilePath = getBundleFilePath;
|
|
@@ -37,6 +71,7 @@ const react_native_1 = require("react-native");
|
|
|
37
71
|
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
38
72
|
const config_1 = require("./config");
|
|
39
73
|
const zipExtract_1 = require("./zipExtract");
|
|
74
|
+
const deviceProfile_1 = require("./deviceProfile");
|
|
40
75
|
const types_1 = require("./types");
|
|
41
76
|
const initialProgressState = {
|
|
42
77
|
visible: false,
|
|
@@ -75,6 +110,7 @@ exports.defaultMessages = {
|
|
|
75
110
|
installing: 'در حال نصب به\u200cروزرسانی...',
|
|
76
111
|
restarting: 'در حال راه\u200cاندازی مجدد...',
|
|
77
112
|
error: 'خطا در به\u200cروزرسانی',
|
|
113
|
+
downloadInterrupted: 'دانلود قطع شد. لطفاً دوباره تلاش کنید',
|
|
78
114
|
upToDate: 'برنامه به\u200cروز است',
|
|
79
115
|
updateAvailable: 'نسخه جدیدی در دسترس است',
|
|
80
116
|
changelogTitle: 'تغییرات این نسخه',
|
|
@@ -254,8 +290,25 @@ async function getDeviceAbi() {
|
|
|
254
290
|
}
|
|
255
291
|
return undefined;
|
|
256
292
|
}
|
|
293
|
+
/** FCM token from the library Firebase project (shared config, native). */
|
|
294
|
+
async function getFcmToken() {
|
|
295
|
+
if (!OTAUpdaterNative?.getFcmToken) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const token = await OTAUpdaterNative.getFcmToken();
|
|
300
|
+
return typeof token === 'string' && token.trim() ? token.trim() : undefined;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
257
306
|
async function checkForUpdate(serverUrl, deploymentKey, appVersion, label, packageHash, clientUniqueId, buildNumber, deviceAbi) {
|
|
258
307
|
const baseUrl = serverUrl.replace(/\/$/, '');
|
|
308
|
+
const [profile, fcmToken] = await Promise.all([
|
|
309
|
+
(0, deviceProfile_1.getDeviceProfile)(),
|
|
310
|
+
getFcmToken(),
|
|
311
|
+
]);
|
|
259
312
|
const response = await fetch(`${baseUrl}/api/update-check`, {
|
|
260
313
|
method: 'POST',
|
|
261
314
|
headers: {
|
|
@@ -268,7 +321,9 @@ async function checkForUpdate(serverUrl, deploymentKey, appVersion, label, packa
|
|
|
268
321
|
packageHash,
|
|
269
322
|
clientUniqueId,
|
|
270
323
|
buildNumber,
|
|
271
|
-
|
|
324
|
+
...profile,
|
|
325
|
+
deviceAbi: deviceAbi ?? profile.deviceAbi,
|
|
326
|
+
fcmToken,
|
|
272
327
|
}),
|
|
273
328
|
});
|
|
274
329
|
if (!response.ok) {
|
|
@@ -413,6 +468,31 @@ async function ensureBundleDirectory() {
|
|
|
413
468
|
await ReactNativeBlobUtil.fs.mkdir(dir);
|
|
414
469
|
}
|
|
415
470
|
}
|
|
471
|
+
/** Keep only the active OTA package so leftover extracts don't fill disk / slow IO. */
|
|
472
|
+
async function cleanupOldUpdates(keepLabel) {
|
|
473
|
+
try {
|
|
474
|
+
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
475
|
+
const dir = getBundleDirectory();
|
|
476
|
+
const exists = await ReactNativeBlobUtil.fs.exists(dir);
|
|
477
|
+
if (!exists)
|
|
478
|
+
return;
|
|
479
|
+
const keep = new Set([
|
|
480
|
+
keepLabel,
|
|
481
|
+
`${keepLabel}.zip`,
|
|
482
|
+
`${keepLabel}.bundle`,
|
|
483
|
+
]);
|
|
484
|
+
const entries = await ReactNativeBlobUtil.fs.ls(dir);
|
|
485
|
+
await Promise.all(entries.map(async (entry) => {
|
|
486
|
+
if (keep.has(entry) || entry.endsWith('.apk')) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await removePathRecursive(`${dir}/${entry}`);
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Best-effort cleanup; a leftover folder must not fail install.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
416
496
|
async function resolveDownloadSize(downloadUrl, deploymentKey, expectedSize, deviceAbi) {
|
|
417
497
|
if (expectedSize && expectedSize > 0) {
|
|
418
498
|
return expectedSize;
|
|
@@ -449,6 +529,80 @@ async function resolveDownloadSize(downloadUrl, deploymentKey, expectedSize, dev
|
|
|
449
529
|
}
|
|
450
530
|
return expectedSize ?? 0;
|
|
451
531
|
}
|
|
532
|
+
const DOWNLOAD_MAX_ATTEMPTS = 3;
|
|
533
|
+
function getErrorMessage(error) {
|
|
534
|
+
if (error instanceof Error)
|
|
535
|
+
return error.message;
|
|
536
|
+
if (typeof error === 'string')
|
|
537
|
+
return error;
|
|
538
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
539
|
+
return String(error.message);
|
|
540
|
+
}
|
|
541
|
+
return String(error);
|
|
542
|
+
}
|
|
543
|
+
function isRetryableDownloadError(error) {
|
|
544
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
545
|
+
return (message.includes('download interrupted') ||
|
|
546
|
+
message.includes('network') ||
|
|
547
|
+
message.includes('timeout') ||
|
|
548
|
+
message.includes('timed out') ||
|
|
549
|
+
message.includes('connection') ||
|
|
550
|
+
message.includes('socket') ||
|
|
551
|
+
message.includes('econnreset') ||
|
|
552
|
+
message.includes('failed to connect') ||
|
|
553
|
+
message.includes('software caused connection abort'));
|
|
554
|
+
}
|
|
555
|
+
function toUserFacingDownloadError(error) {
|
|
556
|
+
if (isRetryableDownloadError(error)) {
|
|
557
|
+
return new Error(exports.defaultMessages.downloadInterrupted);
|
|
558
|
+
}
|
|
559
|
+
return error instanceof Error ? error : new Error(getErrorMessage(error));
|
|
560
|
+
}
|
|
561
|
+
async function cleanupPartialDownload(path) {
|
|
562
|
+
try {
|
|
563
|
+
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
564
|
+
const exists = await ReactNativeBlobUtil.fs.exists(path);
|
|
565
|
+
if (exists) {
|
|
566
|
+
await ReactNativeBlobUtil.fs.unlink(path);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Ignore cleanup errors.
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* react-native-blob-util rejects with "Download interrupted." whenever
|
|
575
|
+
* bytesWritten !== Content-Length — including when Content-Length is wrong
|
|
576
|
+
* but the full file actually landed. Recover those cases via size/hash checks.
|
|
577
|
+
*/
|
|
578
|
+
async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label) {
|
|
579
|
+
try {
|
|
580
|
+
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
581
|
+
const exists = await ReactNativeBlobUtil.fs.exists(destPath);
|
|
582
|
+
if (!exists)
|
|
583
|
+
return null;
|
|
584
|
+
const stat = await ReactNativeBlobUtil.fs.stat(destPath);
|
|
585
|
+
const minSize = extension === 'apk' ? 1024 * 100 : 1024;
|
|
586
|
+
if (!stat || stat.size < minSize)
|
|
587
|
+
return null;
|
|
588
|
+
const sizeLooksComplete = totalBytes <= 0 || Math.abs(stat.size - totalBytes) <= 1;
|
|
589
|
+
if (expectedHash) {
|
|
590
|
+
const valid = await verifyBundleHash(destPath, expectedHash);
|
|
591
|
+
if (!valid)
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
else if (!sizeLooksComplete) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
|
|
598
|
+
return extractBundleUpdate(label, destPath);
|
|
599
|
+
}
|
|
600
|
+
return destPath;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
452
606
|
async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, onProgress, extension = 'bundle', deviceAbi, packageFormat = 'bundle', expectedHash) {
|
|
453
607
|
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
454
608
|
const isZipBundle = extension === 'bundle' && packageFormat === 'zip';
|
|
@@ -462,76 +616,95 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
|
|
|
462
616
|
if (isZipBundle) {
|
|
463
617
|
await removePathRecursive(getBundleReleaseDir(label));
|
|
464
618
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
});
|
|
477
|
-
const reportDownloadProgress = (received, total) => {
|
|
478
|
-
if (!onProgress)
|
|
479
|
-
return;
|
|
480
|
-
const resolvedTotal = total > 0 ? total : totalBytes;
|
|
481
|
-
if (resolvedTotal <= 0)
|
|
482
|
-
return;
|
|
483
|
-
onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99));
|
|
484
|
-
};
|
|
485
|
-
let pollTimer = null;
|
|
486
|
-
if (onProgress && totalBytes > 0) {
|
|
487
|
-
pollTimer = setInterval(async () => {
|
|
488
|
-
try {
|
|
489
|
-
const exists = await ReactNativeBlobUtil.fs.exists(destPath);
|
|
490
|
-
if (!exists)
|
|
491
|
-
return;
|
|
492
|
-
const stat = await ReactNativeBlobUtil.fs.stat(destPath);
|
|
493
|
-
reportDownloadProgress(stat.size, totalBytes);
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
// Ignore polling errors while the file is being written.
|
|
497
|
-
}
|
|
498
|
-
}, 150);
|
|
499
|
-
}
|
|
500
|
-
if (onProgress) {
|
|
501
|
-
task.progress({ interval: 100, count: 10 }, (received, total) => {
|
|
502
|
-
reportDownloadProgress(received, total);
|
|
619
|
+
let lastError;
|
|
620
|
+
for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
|
|
621
|
+
await cleanupPartialDownload(destPath);
|
|
622
|
+
const task = ReactNativeBlobUtil.config({
|
|
623
|
+
path: destPath,
|
|
624
|
+
fileCache: false,
|
|
625
|
+
overwrite: true,
|
|
626
|
+
timeout: 10 * 60 * 1000,
|
|
627
|
+
}).fetch('GET', downloadUrl, {
|
|
628
|
+
'X-Deployment-Key': deploymentKey,
|
|
629
|
+
...(deviceAbi ? { 'X-Device-Abi': deviceAbi } : {}),
|
|
503
630
|
});
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
631
|
+
const reportDownloadProgress = (received, total) => {
|
|
632
|
+
if (!onProgress)
|
|
633
|
+
return;
|
|
634
|
+
const resolvedTotal = total > 0 ? total : totalBytes;
|
|
635
|
+
if (resolvedTotal <= 0)
|
|
636
|
+
return;
|
|
637
|
+
onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99));
|
|
638
|
+
};
|
|
639
|
+
let pollTimer = null;
|
|
640
|
+
if (onProgress && totalBytes > 0) {
|
|
641
|
+
pollTimer = setInterval(async () => {
|
|
642
|
+
try {
|
|
643
|
+
const exists = await ReactNativeBlobUtil.fs.exists(destPath);
|
|
644
|
+
if (!exists)
|
|
645
|
+
return;
|
|
646
|
+
const stat = await ReactNativeBlobUtil.fs.stat(destPath);
|
|
647
|
+
reportDownloadProgress(stat.size, totalBytes);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Ignore polling errors while the file is being written.
|
|
651
|
+
}
|
|
652
|
+
}, 150);
|
|
513
653
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
654
|
+
if (onProgress) {
|
|
655
|
+
task.progress({ interval: 100, count: 10 }, (received, total) => {
|
|
656
|
+
reportDownloadProgress(received, total);
|
|
657
|
+
});
|
|
518
658
|
}
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
if (
|
|
522
|
-
|
|
659
|
+
try {
|
|
660
|
+
const response = await task;
|
|
661
|
+
if (onProgress) {
|
|
662
|
+
onProgress(1);
|
|
663
|
+
}
|
|
664
|
+
const downloadedPath = String(response.path()).replace(/^file:\/\//, '');
|
|
665
|
+
if (downloadedPath !== destPath) {
|
|
666
|
+
await ReactNativeBlobUtil.fs.cp(downloadedPath, destPath);
|
|
523
667
|
}
|
|
668
|
+
const stat = await ReactNativeBlobUtil.fs.stat(destPath);
|
|
669
|
+
const minSize = extension === 'apk' ? 1024 * 100 : 1024;
|
|
670
|
+
if (!stat || stat.size < minSize) {
|
|
671
|
+
throw new Error(`Downloaded file is too small (${stat?.size ?? 0} bytes)`);
|
|
672
|
+
}
|
|
673
|
+
if (expectedHash) {
|
|
674
|
+
const valid = await verifyBundleHash(destPath, expectedHash);
|
|
675
|
+
if (!valid) {
|
|
676
|
+
throw new Error('Package hash verification failed');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
|
|
680
|
+
return extractBundleUpdate(label, destPath);
|
|
681
|
+
}
|
|
682
|
+
return destPath;
|
|
524
683
|
}
|
|
525
|
-
|
|
526
|
-
|
|
684
|
+
catch (error) {
|
|
685
|
+
lastError = error;
|
|
686
|
+
if (isRetryableDownloadError(error)) {
|
|
687
|
+
const recovered = await tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label);
|
|
688
|
+
if (recovered) {
|
|
689
|
+
if (onProgress)
|
|
690
|
+
onProgress(1);
|
|
691
|
+
return recovered;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
await cleanupPartialDownload(destPath);
|
|
695
|
+
if (attempt < DOWNLOAD_MAX_ATTEMPTS && isRetryableDownloadError(error)) {
|
|
696
|
+
await delay(attempt * 750);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
throw toUserFacingDownloadError(error);
|
|
527
700
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
clearInterval(pollTimer);
|
|
701
|
+
finally {
|
|
702
|
+
if (pollTimer) {
|
|
703
|
+
clearInterval(pollTimer);
|
|
704
|
+
}
|
|
533
705
|
}
|
|
534
706
|
}
|
|
707
|
+
throw toUserFacingDownloadError(lastError);
|
|
535
708
|
}
|
|
536
709
|
async function verifyBundleHash(filePath, expectedHash) {
|
|
537
710
|
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
@@ -725,6 +898,9 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
725
898
|
packageHash: update.packageHash,
|
|
726
899
|
installedAt: new Date().toISOString(),
|
|
727
900
|
});
|
|
901
|
+
void Promise.resolve().then(() => __importStar(require('./telemetry'))).then((mod) => mod.ping())
|
|
902
|
+
.catch(() => { });
|
|
903
|
+
await cleanupOldUpdates(update.label);
|
|
728
904
|
const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
|
|
729
905
|
if (shouldRestartNow) {
|
|
730
906
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.RESTARTING, 1, {
|
|
@@ -793,12 +969,8 @@ async function performApkSync(update, showUI, messages, onProgress, changelog) {
|
|
|
793
969
|
label: update.label,
|
|
794
970
|
description: notes,
|
|
795
971
|
isMandatory: update.isMandatory,
|
|
796
|
-
}), 'apk', update.deviceAbi);
|
|
972
|
+
}), 'apk', update.deviceAbi, 'bundle', update.packageHash);
|
|
797
973
|
onProgress?.(types_1.SyncStatus.INSTALLING, 1);
|
|
798
|
-
const valid = await verifyBundleHash(apkPath, update.packageHash);
|
|
799
|
-
if (!valid) {
|
|
800
|
-
throw new Error('APK hash verification failed');
|
|
801
|
-
}
|
|
802
974
|
// Drop the OTA JS path before opening the installer so the new APK does not
|
|
803
975
|
// keep loading the previous bundle on first launch after install.
|
|
804
976
|
await clearUpdate();
|
|
@@ -855,10 +1027,16 @@ async function getInstalledBundlePath() {
|
|
|
855
1027
|
return null;
|
|
856
1028
|
}
|
|
857
1029
|
async function clearUpdate() {
|
|
1030
|
+
const meta = await getStoredBundleMeta();
|
|
858
1031
|
await clearStoredBundleMeta();
|
|
859
1032
|
if (OTAUpdaterNative?.clearBundlePath) {
|
|
860
1033
|
await OTAUpdaterNative.clearBundlePath();
|
|
861
1034
|
}
|
|
1035
|
+
if (meta?.label) {
|
|
1036
|
+
await removePathRecursive(getBundleReleaseDir(meta.label));
|
|
1037
|
+
await cleanupPartialDownload(getBundleZipPath(meta.label));
|
|
1038
|
+
await cleanupPartialDownload(getLegacyBundleFilePath(meta.label));
|
|
1039
|
+
}
|
|
862
1040
|
}
|
|
863
1041
|
async function notifyAppReady() {
|
|
864
1042
|
const meta = await getStoredBundleMeta();
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ping = ping;
|
|
7
|
+
exports.startTelemetry = startTelemetry;
|
|
8
|
+
exports.stopTelemetry = stopTelemetry;
|
|
9
|
+
const react_native_1 = require("react-native");
|
|
10
|
+
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const deviceProfile_1 = require("./deviceProfile");
|
|
13
|
+
const sync_1 = require("./sync");
|
|
14
|
+
const PING_INTERVAL_MS = 60000;
|
|
15
|
+
let started = false;
|
|
16
|
+
let pingTimer = null;
|
|
17
|
+
let appStateSub = null;
|
|
18
|
+
let pingInFlight = null;
|
|
19
|
+
async function buildPingBody(sessionStart) {
|
|
20
|
+
const config = (0, config_1.getConfig)();
|
|
21
|
+
if (!config) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const [clientUniqueId, meta, profile, fcmToken] = await Promise.all([
|
|
25
|
+
(0, sync_1.getClientUniqueId)(),
|
|
26
|
+
(0, sync_1.getStoredBundleMeta)(),
|
|
27
|
+
(0, deviceProfile_1.getDeviceProfile)(),
|
|
28
|
+
(0, sync_1.getFcmToken)(),
|
|
29
|
+
]);
|
|
30
|
+
return {
|
|
31
|
+
clientUniqueId,
|
|
32
|
+
appVersion: react_native_device_info_1.default.getVersion(),
|
|
33
|
+
buildNumber: react_native_device_info_1.default.getBuildNumber(),
|
|
34
|
+
...profile,
|
|
35
|
+
label: meta?.label,
|
|
36
|
+
packageHash: meta?.packageHash,
|
|
37
|
+
fcmToken,
|
|
38
|
+
sessionStart,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function ping(options = {}) {
|
|
42
|
+
const config = (0, config_1.getConfig)();
|
|
43
|
+
if (!config) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (pingInFlight) {
|
|
47
|
+
return pingInFlight;
|
|
48
|
+
}
|
|
49
|
+
pingInFlight = (async () => {
|
|
50
|
+
try {
|
|
51
|
+
const body = await buildPingBody(options.sessionStart === true);
|
|
52
|
+
if (!body) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const baseUrl = (0, config_1.getServerUrl)(config);
|
|
56
|
+
await fetch(`${baseUrl}/api/devices/ping`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'X-Deployment-Key': config.deploymentKey,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Telemetry must never break update sync.
|
|
67
|
+
}
|
|
68
|
+
})().finally(() => {
|
|
69
|
+
pingInFlight = null;
|
|
70
|
+
});
|
|
71
|
+
return pingInFlight;
|
|
72
|
+
}
|
|
73
|
+
function startTelemetry() {
|
|
74
|
+
if (started) {
|
|
75
|
+
return stopTelemetry;
|
|
76
|
+
}
|
|
77
|
+
started = true;
|
|
78
|
+
void ping({ sessionStart: true });
|
|
79
|
+
pingTimer = setInterval(() => {
|
|
80
|
+
void ping();
|
|
81
|
+
}, PING_INTERVAL_MS);
|
|
82
|
+
appStateSub = react_native_1.AppState.addEventListener('change', (nextState) => {
|
|
83
|
+
if (nextState === 'active') {
|
|
84
|
+
void ping();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return stopTelemetry;
|
|
88
|
+
}
|
|
89
|
+
function stopTelemetry() {
|
|
90
|
+
started = false;
|
|
91
|
+
if (pingTimer) {
|
|
92
|
+
clearInterval(pingTimer);
|
|
93
|
+
pingTimer = null;
|
|
94
|
+
}
|
|
95
|
+
appStateSub?.remove();
|
|
96
|
+
appStateSub = null;
|
|
97
|
+
}
|
package/lib/types.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ const react_native_1 = require("react-native");
|
|
|
39
39
|
const projectConfig_1 = require("../projectConfig");
|
|
40
40
|
const progress_1 = require("../progress");
|
|
41
41
|
const sync_1 = require("../sync");
|
|
42
|
+
const telemetry_1 = require("../telemetry");
|
|
42
43
|
const OTAUpdateModal_1 = require("./OTAUpdateModal");
|
|
43
44
|
const SYNC_DELAY_MS = 2500;
|
|
44
45
|
const FOREGROUND_SYNC_DEBOUNCE_MS = 30000;
|
|
@@ -54,6 +55,11 @@ function OTAUpdateProvider({ children, projectConfig, autoSync = true, skipInDev
|
|
|
54
55
|
}
|
|
55
56
|
}, [projectConfig]);
|
|
56
57
|
(0, react_1.useEffect)(() => (0, progress_1.subscribeProgress)(setProgress), []);
|
|
58
|
+
(0, react_1.useEffect)(() => {
|
|
59
|
+
if (skipInDev && __DEV__)
|
|
60
|
+
return;
|
|
61
|
+
return (0, telemetry_1.startTelemetry)();
|
|
62
|
+
}, [skipInDev]);
|
|
57
63
|
(0, react_1.useEffect)(() => {
|
|
58
64
|
if (!autoSync)
|
|
59
65
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsonair.ir/react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "React Native client SDK for self-hosted OTA updates",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"lib",
|
|
10
10
|
"android",
|
|
11
11
|
"ios",
|
|
12
|
+
"firebase",
|
|
13
|
+
"appsonair-react-native.podspec",
|
|
12
14
|
"react-native.config.js"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|