@appsonair.ir/react-native 1.0.3 → 1.0.5
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 +7 -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 +2 -0
- package/lib/sync.js +57 -1
- 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"
|
|
@@ -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
|
@@ -48,6 +48,8 @@ export declare function clearStoredBundleMeta(): Promise<void>;
|
|
|
48
48
|
export declare function markRestartPending(): Promise<void>;
|
|
49
49
|
export declare function shouldSkipSyncAfterRestart(): Promise<boolean>;
|
|
50
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>;
|
|
51
53
|
export declare function checkForUpdate(serverUrl: string, deploymentKey: string, appVersion: string, label?: string, packageHash?: string, clientUniqueId?: string, buildNumber?: string, deviceAbi?: string): Promise<UpdateCheckResult>;
|
|
52
54
|
export declare function getBundleReleaseDir(label: string): string;
|
|
53
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,
|
|
@@ -255,8 +290,25 @@ async function getDeviceAbi() {
|
|
|
255
290
|
}
|
|
256
291
|
return undefined;
|
|
257
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
|
+
}
|
|
258
306
|
async function checkForUpdate(serverUrl, deploymentKey, appVersion, label, packageHash, clientUniqueId, buildNumber, deviceAbi) {
|
|
259
307
|
const baseUrl = serverUrl.replace(/\/$/, '');
|
|
308
|
+
const [profile, fcmToken] = await Promise.all([
|
|
309
|
+
(0, deviceProfile_1.getDeviceProfile)(),
|
|
310
|
+
getFcmToken(),
|
|
311
|
+
]);
|
|
260
312
|
const response = await fetch(`${baseUrl}/api/update-check`, {
|
|
261
313
|
method: 'POST',
|
|
262
314
|
headers: {
|
|
@@ -269,7 +321,9 @@ async function checkForUpdate(serverUrl, deploymentKey, appVersion, label, packa
|
|
|
269
321
|
packageHash,
|
|
270
322
|
clientUniqueId,
|
|
271
323
|
buildNumber,
|
|
272
|
-
|
|
324
|
+
...profile,
|
|
325
|
+
deviceAbi: deviceAbi ?? profile.deviceAbi,
|
|
326
|
+
fcmToken,
|
|
273
327
|
}),
|
|
274
328
|
});
|
|
275
329
|
if (!response.ok) {
|
|
@@ -844,6 +898,8 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
844
898
|
packageHash: update.packageHash,
|
|
845
899
|
installedAt: new Date().toISOString(),
|
|
846
900
|
});
|
|
901
|
+
void Promise.resolve().then(() => __importStar(require('./telemetry'))).then((mod) => mod.ping())
|
|
902
|
+
.catch(() => { });
|
|
847
903
|
await cleanupOldUpdates(update.label);
|
|
848
904
|
const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
|
|
849
905
|
if (shouldRestartNow) {
|
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.5",
|
|
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": {
|