rampkit-expo-dev 0.0.19 → 0.0.23

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.
@@ -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 (using legacy APIs for iOS 13+ compatibility)
128
+ "deviceLanguageCode": locale.languageCode,
129
+ "deviceLocale": locale.identifier,
130
+ "regionCode": locale.regionCode,
131
+ "preferredLanguage": locales.first,
132
+ "preferredLanguages": locales,
133
+
134
+ // Currency
135
+ "deviceCurrencyCode": locale.currencyCode,
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.19",
3
+ "version": "0.0.23",
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-haptics": "*",
39
- "expo-store-review": "*",
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": "*"