@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.
@@ -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
+ }
@@ -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
+ }
@@ -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
- deviceAbi,
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) {
@@ -0,0 +1,6 @@
1
+ export interface TelemetryPingOptions {
2
+ sessionStart?: boolean;
3
+ }
4
+ export declare function ping(options?: TelemetryPingOptions): Promise<void>;
5
+ export declare function startTelemetry(): () => void;
6
+ export declare function stopTelemetry(): void;
@@ -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
@@ -117,3 +117,5 @@ export interface StoredBundleMeta {
117
117
  packageHash: string;
118
118
  installedAt: string;
119
119
  }
120
+ /** Primitive values allowed on custom analytics events. */
121
+ export type AnalyticsEventParams = Record<string, string | number | boolean>;
@@ -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",
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": {