rampkit-expo-dev 0.0.18 → 0.0.22
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 +87 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/expo/modules/rampkit/RampKitModule.kt +445 -0
- package/build/DeviceInfoCollector.d.ts +21 -0
- package/build/DeviceInfoCollector.js +200 -0
- package/build/EventManager.d.ts +155 -0
- package/build/EventManager.js +419 -0
- package/build/RampKit.d.ts +93 -9
- package/build/RampKit.js +224 -21
- package/build/RampKitNative.d.ts +151 -0
- package/build/RampKitNative.js +255 -0
- package/build/RampkitOverlay.d.ts +8 -0
- package/build/RampkitOverlay.js +72 -106
- package/build/constants.d.ts +18 -0
- package/build/constants.js +29 -0
- package/build/index.d.ts +12 -0
- package/build/index.js +30 -1
- package/build/types.d.ts +178 -0
- package/build/types.js +5 -0
- package/build/userId.d.ts +8 -0
- package/build/userId.js +28 -72
- package/expo-module.config.json +10 -0
- package/ios/RampKit.podspec +22 -0
- package/ios/RampKitModule.swift +407 -0
- package/package.json +9 -7
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
import Security
|
|
4
|
+
import StoreKit
|
|
5
|
+
|
|
6
|
+
public class RampKitModule: Module {
|
|
7
|
+
// Storage keys
|
|
8
|
+
private let userIdKey = "rk_user_id"
|
|
9
|
+
private let installDateKey = "rk_install_date"
|
|
10
|
+
private let launchCountKey = "rk_launch_count"
|
|
11
|
+
private let lastLaunchKey = "rk_last_launch"
|
|
12
|
+
|
|
13
|
+
public func definition() -> ModuleDefinition {
|
|
14
|
+
Name("RampKit")
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Device Info
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
AsyncFunction("getDeviceInfo") { () -> [String: Any?] in
|
|
21
|
+
return self.collectDeviceInfo()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AsyncFunction("getUserId") { () -> String in
|
|
25
|
+
return self.getOrCreateUserId()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
AsyncFunction("getStoredValue") { (key: String) -> String? in
|
|
29
|
+
return UserDefaults.standard.string(forKey: key)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AsyncFunction("setStoredValue") { (key: String, value: String) in
|
|
33
|
+
UserDefaults.standard.set(value, forKey: key)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
AsyncFunction("getLaunchTrackingData") { () -> [String: Any?] in
|
|
37
|
+
return self.getLaunchTrackingData()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Haptics
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
AsyncFunction("impactAsync") { (style: String) in
|
|
45
|
+
self.performImpactHaptic(style: style)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
AsyncFunction("notificationAsync") { (type: String) in
|
|
49
|
+
self.performNotificationHaptic(type: type)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
AsyncFunction("selectionAsync") { () in
|
|
53
|
+
self.performSelectionHaptic()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Store Review
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
AsyncFunction("requestReview") { () in
|
|
61
|
+
self.requestStoreReview()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
AsyncFunction("isReviewAvailable") { () -> Bool in
|
|
65
|
+
return true // Always available on iOS
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
AsyncFunction("getStoreUrl") { () -> String? in
|
|
69
|
+
// Return nil - app should provide its own store URL
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Notifications
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
AsyncFunction("requestNotificationPermissions") { (options: [String: Any]?) -> [String: Any] in
|
|
78
|
+
return await self.requestNotificationPermissions(options: options)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
AsyncFunction("getNotificationPermissions") { () -> [String: Any] in
|
|
82
|
+
return await self.getNotificationPermissions()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Device Info Collection
|
|
87
|
+
|
|
88
|
+
private func collectDeviceInfo() -> [String: Any?] {
|
|
89
|
+
let device = UIDevice.current
|
|
90
|
+
let screen = UIScreen.main
|
|
91
|
+
let bundle = Bundle.main
|
|
92
|
+
let locale = Locale.current
|
|
93
|
+
let timezone = TimeZone.current
|
|
94
|
+
|
|
95
|
+
let userId = getOrCreateUserId()
|
|
96
|
+
let launchData = getLaunchTrackingData()
|
|
97
|
+
let locales = Locale.preferredLanguages
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
// User & Session
|
|
101
|
+
"appUserId": userId,
|
|
102
|
+
"vendorId": device.identifierForVendor?.uuidString,
|
|
103
|
+
"appSessionId": UUID().uuidString.lowercased(),
|
|
104
|
+
|
|
105
|
+
// Launch tracking
|
|
106
|
+
"installDate": launchData["installDate"],
|
|
107
|
+
"isFirstLaunch": launchData["isFirstLaunch"],
|
|
108
|
+
"launchCount": launchData["launchCount"],
|
|
109
|
+
"lastLaunchAt": launchData["lastLaunchAt"],
|
|
110
|
+
|
|
111
|
+
// App info
|
|
112
|
+
"bundleId": bundle.bundleIdentifier,
|
|
113
|
+
"appName": bundle.infoDictionary?["CFBundleDisplayName"] as? String
|
|
114
|
+
?? bundle.infoDictionary?["CFBundleName"] as? String,
|
|
115
|
+
"appVersion": bundle.infoDictionary?["CFBundleShortVersionString"] as? String,
|
|
116
|
+
"buildNumber": bundle.infoDictionary?["CFBundleVersion"] as? String,
|
|
117
|
+
|
|
118
|
+
// Platform
|
|
119
|
+
"platform": isPad() ? "iPadOS" : "iOS",
|
|
120
|
+
"platformVersion": device.systemVersion,
|
|
121
|
+
|
|
122
|
+
// Device
|
|
123
|
+
"deviceModel": getDeviceModelIdentifier(),
|
|
124
|
+
"deviceName": device.model,
|
|
125
|
+
"isSimulator": isSimulator(),
|
|
126
|
+
|
|
127
|
+
// Locale
|
|
128
|
+
"deviceLanguageCode": locale.language.languageCode?.identifier,
|
|
129
|
+
"deviceLocale": locale.identifier,
|
|
130
|
+
"regionCode": locale.region?.identifier,
|
|
131
|
+
"preferredLanguage": locales.first,
|
|
132
|
+
"preferredLanguages": locales,
|
|
133
|
+
|
|
134
|
+
// Currency
|
|
135
|
+
"deviceCurrencyCode": locale.currency?.identifier,
|
|
136
|
+
"deviceCurrencySymbol": locale.currencySymbol,
|
|
137
|
+
|
|
138
|
+
// Timezone
|
|
139
|
+
"timezoneIdentifier": timezone.identifier,
|
|
140
|
+
"timezoneOffsetSeconds": timezone.secondsFromGMT(),
|
|
141
|
+
|
|
142
|
+
// UI
|
|
143
|
+
"interfaceStyle": getInterfaceStyle(),
|
|
144
|
+
|
|
145
|
+
// Screen
|
|
146
|
+
"screenWidth": screen.bounds.width,
|
|
147
|
+
"screenHeight": screen.bounds.height,
|
|
148
|
+
"screenScale": screen.scale,
|
|
149
|
+
|
|
150
|
+
// Device status
|
|
151
|
+
"isLowPowerMode": ProcessInfo.processInfo.isLowPowerModeEnabled,
|
|
152
|
+
|
|
153
|
+
// Memory
|
|
154
|
+
"totalMemoryBytes": ProcessInfo.processInfo.physicalMemory,
|
|
155
|
+
|
|
156
|
+
// Timestamp
|
|
157
|
+
"collectedAt": ISO8601DateFormatter().string(from: Date())
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - User ID Management (Keychain)
|
|
162
|
+
|
|
163
|
+
private func getOrCreateUserId() -> String {
|
|
164
|
+
if let existingId = getKeychainValue(forKey: userIdKey) {
|
|
165
|
+
return existingId
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let newId = UUID().uuidString.lowercased()
|
|
169
|
+
setKeychainValue(newId, forKey: userIdKey)
|
|
170
|
+
return newId
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func getKeychainValue(forKey key: String) -> String? {
|
|
174
|
+
let query: [String: Any] = [
|
|
175
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
176
|
+
kSecAttrAccount as String: key,
|
|
177
|
+
kSecAttrService as String: Bundle.main.bundleIdentifier ?? "com.rampkit.sdk",
|
|
178
|
+
kSecReturnData as String: true,
|
|
179
|
+
kSecMatchLimit as String: kSecMatchLimitOne
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
var result: AnyObject?
|
|
183
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
184
|
+
|
|
185
|
+
if status == errSecSuccess, let data = result as? Data {
|
|
186
|
+
return String(data: data, encoding: .utf8)
|
|
187
|
+
}
|
|
188
|
+
return nil
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func setKeychainValue(_ value: String, forKey key: String) {
|
|
192
|
+
guard let data = value.data(using: .utf8) else { return }
|
|
193
|
+
|
|
194
|
+
let deleteQuery: [String: Any] = [
|
|
195
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
196
|
+
kSecAttrAccount as String: key,
|
|
197
|
+
kSecAttrService as String: Bundle.main.bundleIdentifier ?? "com.rampkit.sdk"
|
|
198
|
+
]
|
|
199
|
+
SecItemDelete(deleteQuery as CFDictionary)
|
|
200
|
+
|
|
201
|
+
let addQuery: [String: Any] = [
|
|
202
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
203
|
+
kSecAttrAccount as String: key,
|
|
204
|
+
kSecAttrService as String: Bundle.main.bundleIdentifier ?? "com.rampkit.sdk",
|
|
205
|
+
kSecValueData as String: data,
|
|
206
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
207
|
+
]
|
|
208
|
+
SecItemAdd(addQuery as CFDictionary, nil)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// MARK: - Launch Tracking
|
|
212
|
+
|
|
213
|
+
private func getLaunchTrackingData() -> [String: Any?] {
|
|
214
|
+
let defaults = UserDefaults.standard
|
|
215
|
+
let now = ISO8601DateFormatter().string(from: Date())
|
|
216
|
+
|
|
217
|
+
let existingInstallDate = defaults.string(forKey: installDateKey)
|
|
218
|
+
let isFirstLaunch = existingInstallDate == nil
|
|
219
|
+
let installDate = existingInstallDate ?? now
|
|
220
|
+
|
|
221
|
+
let lastLaunchAt = defaults.string(forKey: lastLaunchKey)
|
|
222
|
+
let launchCount = defaults.integer(forKey: launchCountKey) + 1
|
|
223
|
+
|
|
224
|
+
if isFirstLaunch {
|
|
225
|
+
defaults.set(installDate, forKey: installDateKey)
|
|
226
|
+
}
|
|
227
|
+
defaults.set(launchCount, forKey: launchCountKey)
|
|
228
|
+
defaults.set(now, forKey: lastLaunchKey)
|
|
229
|
+
|
|
230
|
+
return [
|
|
231
|
+
"installDate": installDate,
|
|
232
|
+
"isFirstLaunch": isFirstLaunch,
|
|
233
|
+
"launchCount": launchCount,
|
|
234
|
+
"lastLaunchAt": lastLaunchAt
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// MARK: - Haptics
|
|
239
|
+
|
|
240
|
+
private func performImpactHaptic(style: String) {
|
|
241
|
+
let feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle
|
|
242
|
+
switch style.lowercased() {
|
|
243
|
+
case "light":
|
|
244
|
+
feedbackStyle = .light
|
|
245
|
+
case "medium":
|
|
246
|
+
feedbackStyle = .medium
|
|
247
|
+
case "heavy":
|
|
248
|
+
feedbackStyle = .heavy
|
|
249
|
+
case "rigid":
|
|
250
|
+
feedbackStyle = .rigid
|
|
251
|
+
case "soft":
|
|
252
|
+
feedbackStyle = .soft
|
|
253
|
+
default:
|
|
254
|
+
feedbackStyle = .medium
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let generator = UIImpactFeedbackGenerator(style: feedbackStyle)
|
|
258
|
+
generator.prepare()
|
|
259
|
+
generator.impactOccurred()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func performNotificationHaptic(type: String) {
|
|
263
|
+
let feedbackType: UINotificationFeedbackGenerator.FeedbackType
|
|
264
|
+
switch type.lowercased() {
|
|
265
|
+
case "success":
|
|
266
|
+
feedbackType = .success
|
|
267
|
+
case "warning":
|
|
268
|
+
feedbackType = .warning
|
|
269
|
+
case "error":
|
|
270
|
+
feedbackType = .error
|
|
271
|
+
default:
|
|
272
|
+
feedbackType = .success
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let generator = UINotificationFeedbackGenerator()
|
|
276
|
+
generator.prepare()
|
|
277
|
+
generator.notificationOccurred(feedbackType)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func performSelectionHaptic() {
|
|
281
|
+
let generator = UISelectionFeedbackGenerator()
|
|
282
|
+
generator.prepare()
|
|
283
|
+
generator.selectionChanged()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// MARK: - Store Review
|
|
287
|
+
|
|
288
|
+
private func requestStoreReview() {
|
|
289
|
+
DispatchQueue.main.async {
|
|
290
|
+
if #available(iOS 14.0, *) {
|
|
291
|
+
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
|
292
|
+
SKStoreReviewController.requestReview(in: scene)
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
SKStoreReviewController.requestReview()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// MARK: - Notifications
|
|
301
|
+
|
|
302
|
+
private func requestNotificationPermissions(options: [String: Any]?) async -> [String: Any] {
|
|
303
|
+
let center = UNUserNotificationCenter.current()
|
|
304
|
+
|
|
305
|
+
var authOptions: UNAuthorizationOptions = []
|
|
306
|
+
|
|
307
|
+
if let ios = options?["ios"] as? [String: Any] {
|
|
308
|
+
if ios["allowAlert"] as? Bool ?? true { authOptions.insert(.alert) }
|
|
309
|
+
if ios["allowBadge"] as? Bool ?? true { authOptions.insert(.badge) }
|
|
310
|
+
if ios["allowSound"] as? Bool ?? true { authOptions.insert(.sound) }
|
|
311
|
+
} else {
|
|
312
|
+
authOptions = [.alert, .badge, .sound]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
do {
|
|
316
|
+
let granted = try await center.requestAuthorization(options: authOptions)
|
|
317
|
+
let settings = await center.notificationSettings()
|
|
318
|
+
|
|
319
|
+
return [
|
|
320
|
+
"granted": granted,
|
|
321
|
+
"status": mapAuthorizationStatus(settings.authorizationStatus),
|
|
322
|
+
"canAskAgain": settings.authorizationStatus != .denied,
|
|
323
|
+
"ios": [
|
|
324
|
+
"alertSetting": mapNotificationSetting(settings.alertSetting),
|
|
325
|
+
"badgeSetting": mapNotificationSetting(settings.badgeSetting),
|
|
326
|
+
"soundSetting": mapNotificationSetting(settings.soundSetting),
|
|
327
|
+
"lockScreenSetting": mapNotificationSetting(settings.lockScreenSetting),
|
|
328
|
+
"notificationCenterSetting": mapNotificationSetting(settings.notificationCenterSetting)
|
|
329
|
+
]
|
|
330
|
+
]
|
|
331
|
+
} catch {
|
|
332
|
+
return [
|
|
333
|
+
"granted": false,
|
|
334
|
+
"status": "denied",
|
|
335
|
+
"canAskAgain": false,
|
|
336
|
+
"error": error.localizedDescription
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func getNotificationPermissions() async -> [String: Any] {
|
|
342
|
+
let center = UNUserNotificationCenter.current()
|
|
343
|
+
let settings = await center.notificationSettings()
|
|
344
|
+
|
|
345
|
+
return [
|
|
346
|
+
"granted": settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional,
|
|
347
|
+
"status": mapAuthorizationStatus(settings.authorizationStatus),
|
|
348
|
+
"canAskAgain": settings.authorizationStatus != .denied
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private func mapAuthorizationStatus(_ status: UNAuthorizationStatus) -> String {
|
|
353
|
+
switch status {
|
|
354
|
+
case .notDetermined: return "undetermined"
|
|
355
|
+
case .denied: return "denied"
|
|
356
|
+
case .authorized: return "granted"
|
|
357
|
+
case .provisional: return "provisional"
|
|
358
|
+
case .ephemeral: return "ephemeral"
|
|
359
|
+
@unknown default: return "undetermined"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private func mapNotificationSetting(_ setting: UNNotificationSetting) -> String {
|
|
364
|
+
switch setting {
|
|
365
|
+
case .notSupported: return "notSupported"
|
|
366
|
+
case .disabled: return "disabled"
|
|
367
|
+
case .enabled: return "enabled"
|
|
368
|
+
@unknown default: return "disabled"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// MARK: - Device Helpers
|
|
373
|
+
|
|
374
|
+
private func getDeviceModelIdentifier() -> String {
|
|
375
|
+
var systemInfo = utsname()
|
|
376
|
+
uname(&systemInfo)
|
|
377
|
+
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
|
378
|
+
let identifier = machineMirror.children.reduce("") { identifier, element in
|
|
379
|
+
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
|
380
|
+
return identifier + String(UnicodeScalar(UInt8(value)))
|
|
381
|
+
}
|
|
382
|
+
return identifier
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private func isPad() -> Bool {
|
|
386
|
+
return UIDevice.current.userInterfaceIdiom == .pad
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func isSimulator() -> Bool {
|
|
390
|
+
#if targetEnvironment(simulator)
|
|
391
|
+
return true
|
|
392
|
+
#else
|
|
393
|
+
return false
|
|
394
|
+
#endif
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private func getInterfaceStyle() -> String {
|
|
398
|
+
if #available(iOS 13.0, *) {
|
|
399
|
+
switch UITraitCollection.current.userInterfaceStyle {
|
|
400
|
+
case .dark: return "dark"
|
|
401
|
+
case .light: return "light"
|
|
402
|
+
default: return "unspecified"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return "light"
|
|
406
|
+
}
|
|
407
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rampkit-expo-dev",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "The Expo SDK for RampKit. Build, test, and personalize app onboardings with instant updates.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -20,12 +20,17 @@
|
|
|
20
20
|
"rampkit",
|
|
21
21
|
"mobile onboarding",
|
|
22
22
|
"marchitectures",
|
|
23
|
-
"sdk"
|
|
23
|
+
"sdk",
|
|
24
|
+
"analytics",
|
|
25
|
+
"events"
|
|
24
26
|
],
|
|
25
27
|
"author": "RampKit",
|
|
26
28
|
"license": "MIT",
|
|
27
29
|
"files": [
|
|
28
30
|
"build",
|
|
31
|
+
"ios",
|
|
32
|
+
"android",
|
|
33
|
+
"expo-module.config.json",
|
|
29
34
|
"README.md"
|
|
30
35
|
],
|
|
31
36
|
"scripts": {
|
|
@@ -35,11 +40,8 @@
|
|
|
35
40
|
"peerDependencies": {
|
|
36
41
|
"react": "*",
|
|
37
42
|
"react-native": "*",
|
|
38
|
-
"expo
|
|
39
|
-
"expo-
|
|
40
|
-
"expo-notifications": "*",
|
|
41
|
-
"expo-secure-store": "*",
|
|
42
|
-
"expo-crypto": "*",
|
|
43
|
+
"expo": ">=49.0.0",
|
|
44
|
+
"expo-modules-core": "*",
|
|
43
45
|
"react-native-webview": "*",
|
|
44
46
|
"react-native-pager-view": "*",
|
|
45
47
|
"react-native-root-siblings": "*"
|