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,87 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'maven-publish'
4
+
5
+ group = 'expo.modules.rampkit'
6
+ version = '0.0.20'
7
+
8
+ buildscript {
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ if (expoModulesCorePlugin.exists()) {
11
+ apply from: expoModulesCorePlugin
12
+ applyKotlinExpoModulesCorePlugin()
13
+ }
14
+
15
+ repositories {
16
+ mavenCentral()
17
+ }
18
+
19
+ dependencies {
20
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
21
+ }
22
+ }
23
+
24
+ afterEvaluate {
25
+ publishing {
26
+ publications {
27
+ release(MavenPublication) {
28
+ from components.release
29
+ }
30
+ }
31
+ repositories {
32
+ maven {
33
+ url = mavenLocal().url
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ android {
40
+ namespace "expo.modules.rampkit"
41
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
42
+
43
+ defaultConfig {
44
+ minSdkVersion safeExtGet("minSdkVersion", 23)
45
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
46
+ }
47
+
48
+ publishing {
49
+ singleVariant("release") {
50
+ withSourcesJar()
51
+ }
52
+ }
53
+
54
+ lintOptions {
55
+ abortOnError false
56
+ }
57
+
58
+ compileOptions {
59
+ sourceCompatibility JavaVersion.VERSION_17
60
+ targetCompatibility JavaVersion.VERSION_17
61
+ }
62
+
63
+ kotlinOptions {
64
+ jvmTarget = JavaVersion.VERSION_17.majorVersion
65
+ }
66
+ }
67
+
68
+ repositories {
69
+ mavenCentral()
70
+ }
71
+
72
+ dependencies {
73
+ implementation project(':expo-modules-core')
74
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
75
+ implementation 'com.google.android.play:review:2.0.1'
76
+ implementation 'com.google.android.play:review-ktx:2.0.1'
77
+ }
78
+
79
+ def getKotlinVersion() {
80
+ def kotlinVersion = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "1.9.23"
81
+ return kotlinVersion
82
+ }
83
+
84
+ def safeExtGet(prop, fallback) {
85
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
86
+ }
87
+
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
3
+
@@ -0,0 +1,445 @@
1
+ package expo.modules.rampkit
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.app.ActivityManager
6
+ import android.app.NotificationChannel
7
+ import android.app.NotificationManager
8
+ import android.content.Context
9
+ import android.content.SharedPreferences
10
+ import android.content.pm.PackageManager
11
+ import android.content.res.Configuration
12
+ import android.os.Build
13
+ import android.os.PowerManager
14
+ import android.os.VibrationEffect
15
+ import android.os.Vibrator
16
+ import android.os.VibratorManager
17
+ import android.provider.Settings
18
+ import android.util.DisplayMetrics
19
+ import android.view.WindowManager
20
+ import androidx.core.app.ActivityCompat
21
+ import androidx.core.content.ContextCompat
22
+ import com.google.android.play.core.review.ReviewManagerFactory
23
+ import expo.modules.kotlin.Promise
24
+ import expo.modules.kotlin.modules.Module
25
+ import expo.modules.kotlin.modules.ModuleDefinition
26
+ import java.text.SimpleDateFormat
27
+ import java.util.Date
28
+ import java.util.Locale
29
+ import java.util.TimeZone
30
+ import java.util.UUID
31
+
32
+ class RampKitModule : Module() {
33
+ private val PREFS_NAME = "rampkit_prefs"
34
+ private val USER_ID_KEY = "rk_user_id"
35
+ private val INSTALL_DATE_KEY = "rk_install_date"
36
+ private val LAUNCH_COUNT_KEY = "rk_launch_count"
37
+ private val LAST_LAUNCH_KEY = "rk_last_launch"
38
+
39
+ private val context: Context
40
+ get() = requireNotNull(appContext.reactContext)
41
+
42
+ private val prefs: SharedPreferences
43
+ get() = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
44
+
45
+ override fun definition() = ModuleDefinition {
46
+ Name("RampKit")
47
+
48
+ // ============================================================================
49
+ // Device Info
50
+ // ============================================================================
51
+
52
+ AsyncFunction("getDeviceInfo") {
53
+ collectDeviceInfo()
54
+ }
55
+
56
+ AsyncFunction("getUserId") {
57
+ getOrCreateUserId()
58
+ }
59
+
60
+ AsyncFunction("getStoredValue") { key: String ->
61
+ prefs.getString(key, null)
62
+ }
63
+
64
+ AsyncFunction("setStoredValue") { key: String, value: String ->
65
+ prefs.edit().putString(key, value).apply()
66
+ }
67
+
68
+ AsyncFunction("getLaunchTrackingData") {
69
+ getLaunchTrackingData()
70
+ }
71
+
72
+ // ============================================================================
73
+ // Haptics
74
+ // ============================================================================
75
+
76
+ AsyncFunction("impactAsync") { style: String ->
77
+ performImpactHaptic(style)
78
+ }
79
+
80
+ AsyncFunction("notificationAsync") { type: String ->
81
+ performNotificationHaptic(type)
82
+ }
83
+
84
+ AsyncFunction("selectionAsync") {
85
+ performSelectionHaptic()
86
+ }
87
+
88
+ // ============================================================================
89
+ // Store Review
90
+ // ============================================================================
91
+
92
+ AsyncFunction("requestReview") { promise: Promise ->
93
+ requestStoreReview(promise)
94
+ }
95
+
96
+ AsyncFunction("isReviewAvailable") {
97
+ true // Google Play In-App Review is generally available
98
+ }
99
+
100
+ AsyncFunction("getStoreUrl") {
101
+ "https://play.google.com/store/apps/details?id=${context.packageName}"
102
+ }
103
+
104
+ // ============================================================================
105
+ // Notifications
106
+ // ============================================================================
107
+
108
+ AsyncFunction("requestNotificationPermissions") { options: Map<String, Any>? ->
109
+ requestNotificationPermissions(options)
110
+ }
111
+
112
+ AsyncFunction("getNotificationPermissions") {
113
+ getNotificationPermissions()
114
+ }
115
+ }
116
+
117
+ // Device Info Collection
118
+ private fun collectDeviceInfo(): Map<String, Any?> {
119
+ val userId = getOrCreateUserId()
120
+ val launchData = getLaunchTrackingData()
121
+ val displayMetrics = getDisplayMetrics()
122
+ val locale = Locale.getDefault()
123
+ val timezone = TimeZone.getDefault()
124
+ val packageInfo = try {
125
+ context.packageManager.getPackageInfo(context.packageName, 0)
126
+ } catch (e: Exception) {
127
+ null
128
+ }
129
+
130
+ return mapOf(
131
+ "appUserId" to userId,
132
+ "vendorId" to getAndroidId(),
133
+ "appSessionId" to UUID.randomUUID().toString().lowercase(),
134
+ "installDate" to launchData["installDate"],
135
+ "isFirstLaunch" to launchData["isFirstLaunch"],
136
+ "launchCount" to launchData["launchCount"],
137
+ "lastLaunchAt" to launchData["lastLaunchAt"],
138
+ "bundleId" to context.packageName,
139
+ "appName" to getAppName(),
140
+ "appVersion" to packageInfo?.versionName,
141
+ "buildNumber" to getBuildNumber(packageInfo),
142
+ "platform" to "Android",
143
+ "platformVersion" to Build.VERSION.RELEASE,
144
+ "deviceModel" to "${Build.MANUFACTURER} ${Build.MODEL}",
145
+ "deviceName" to Build.MODEL,
146
+ "isSimulator" to isEmulator(),
147
+ "deviceLanguageCode" to locale.language,
148
+ "deviceLocale" to locale.toLanguageTag(),
149
+ "regionCode" to locale.country,
150
+ "preferredLanguage" to locale.toLanguageTag(),
151
+ "preferredLanguages" to listOf(locale.toLanguageTag()),
152
+ "deviceCurrencyCode" to try { java.util.Currency.getInstance(locale).currencyCode } catch (e: Exception) { null },
153
+ "deviceCurrencySymbol" to try { java.util.Currency.getInstance(locale).symbol } catch (e: Exception) { null },
154
+ "timezoneIdentifier" to timezone.id,
155
+ "timezoneOffsetSeconds" to timezone.rawOffset / 1000,
156
+ "interfaceStyle" to getInterfaceStyle(),
157
+ "screenWidth" to (displayMetrics.widthPixels / displayMetrics.density),
158
+ "screenHeight" to (displayMetrics.heightPixels / displayMetrics.density),
159
+ "screenScale" to displayMetrics.density,
160
+ "isLowPowerMode" to isLowPowerMode(),
161
+ "totalMemoryBytes" to getTotalMemory(),
162
+ "collectedAt" to getIso8601Timestamp()
163
+ )
164
+ }
165
+
166
+ private fun getOrCreateUserId(): String {
167
+ val existingId = prefs.getString(USER_ID_KEY, null)
168
+ if (!existingId.isNullOrEmpty()) {
169
+ return existingId
170
+ }
171
+ val newId = UUID.randomUUID().toString().lowercase()
172
+ prefs.edit().putString(USER_ID_KEY, newId).apply()
173
+ return newId
174
+ }
175
+
176
+ private fun getLaunchTrackingData(): Map<String, Any?> {
177
+ val now = getIso8601Timestamp()
178
+ val existingInstallDate = prefs.getString(INSTALL_DATE_KEY, null)
179
+ val isFirstLaunch = existingInstallDate == null
180
+ val installDate = existingInstallDate ?: now
181
+ val lastLaunchAt = prefs.getString(LAST_LAUNCH_KEY, null)
182
+ val launchCount = prefs.getInt(LAUNCH_COUNT_KEY, 0) + 1
183
+
184
+ prefs.edit().apply {
185
+ if (isFirstLaunch) {
186
+ putString(INSTALL_DATE_KEY, installDate)
187
+ }
188
+ putInt(LAUNCH_COUNT_KEY, launchCount)
189
+ putString(LAST_LAUNCH_KEY, now)
190
+ apply()
191
+ }
192
+
193
+ return mapOf(
194
+ "installDate" to installDate,
195
+ "isFirstLaunch" to isFirstLaunch,
196
+ "launchCount" to launchCount,
197
+ "lastLaunchAt" to lastLaunchAt
198
+ )
199
+ }
200
+
201
+ // Haptics
202
+ private fun performImpactHaptic(style: String) {
203
+ val vibrator = getVibrator() ?: return
204
+
205
+ val duration = when (style.lowercase()) {
206
+ "light" -> 10L
207
+ "medium" -> 20L
208
+ "heavy" -> 30L
209
+ "rigid" -> 15L
210
+ "soft" -> 25L
211
+ else -> 20L
212
+ }
213
+
214
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
215
+ val amplitude = when (style.lowercase()) {
216
+ "light" -> 50
217
+ "medium" -> 128
218
+ "heavy" -> 255
219
+ "rigid" -> 200
220
+ "soft" -> 80
221
+ else -> 128
222
+ }
223
+ vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
224
+ } else {
225
+ @Suppress("DEPRECATION")
226
+ vibrator.vibrate(duration)
227
+ }
228
+ }
229
+
230
+ private fun performNotificationHaptic(type: String) {
231
+ val vibrator = getVibrator() ?: return
232
+
233
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
234
+ val effect = when (type.lowercase()) {
235
+ "success" -> VibrationEffect.createWaveform(longArrayOf(0, 50, 50, 50), -1)
236
+ "warning" -> VibrationEffect.createWaveform(longArrayOf(0, 100, 50, 100), -1)
237
+ "error" -> VibrationEffect.createWaveform(longArrayOf(0, 50, 30, 50, 30, 100), -1)
238
+ else -> VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)
239
+ }
240
+ vibrator.vibrate(effect)
241
+ } else {
242
+ @Suppress("DEPRECATION")
243
+ vibrator.vibrate(50)
244
+ }
245
+ }
246
+
247
+ private fun performSelectionHaptic() {
248
+ val vibrator = getVibrator() ?: return
249
+
250
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
251
+ vibrator.vibrate(VibrationEffect.createOneShot(10, 50))
252
+ } else {
253
+ @Suppress("DEPRECATION")
254
+ vibrator.vibrate(10)
255
+ }
256
+ }
257
+
258
+ private fun getVibrator(): Vibrator? {
259
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
260
+ val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager
261
+ vibratorManager?.defaultVibrator
262
+ } else {
263
+ @Suppress("DEPRECATION")
264
+ context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
265
+ }
266
+ }
267
+
268
+ // Store Review
269
+ private fun requestStoreReview(promise: Promise) {
270
+ try {
271
+ val activity = appContext.currentActivity
272
+ if (activity == null) {
273
+ promise.resolve(false)
274
+ return
275
+ }
276
+
277
+ val reviewManager = ReviewManagerFactory.create(context)
278
+ val requestFlow = reviewManager.requestReviewFlow()
279
+
280
+ requestFlow.addOnCompleteListener { task ->
281
+ if (task.isSuccessful) {
282
+ val reviewInfo = task.result
283
+ val flow = reviewManager.launchReviewFlow(activity, reviewInfo)
284
+ flow.addOnCompleteListener {
285
+ promise.resolve(true)
286
+ }
287
+ } else {
288
+ promise.resolve(false)
289
+ }
290
+ }
291
+ } catch (e: Exception) {
292
+ promise.resolve(false)
293
+ }
294
+ }
295
+
296
+ // Notifications
297
+ private fun requestNotificationPermissions(options: Map<String, Any>?): Map<String, Any> {
298
+ // Create notification channel for Android 8+
299
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
300
+ val androidOptions = options?.get("android") as? Map<*, *>
301
+ val channelId = androidOptions?.get("channelId") as? String ?: "default"
302
+ val channelName = androidOptions?.get("name") as? String ?: "Default"
303
+ val importance = when ((androidOptions?.get("importance") as? String)?.uppercase()) {
304
+ "MAX" -> NotificationManager.IMPORTANCE_HIGH
305
+ "HIGH" -> NotificationManager.IMPORTANCE_HIGH
306
+ "DEFAULT" -> NotificationManager.IMPORTANCE_DEFAULT
307
+ "LOW" -> NotificationManager.IMPORTANCE_LOW
308
+ "MIN" -> NotificationManager.IMPORTANCE_MIN
309
+ else -> NotificationManager.IMPORTANCE_HIGH
310
+ }
311
+
312
+ val channel = NotificationChannel(channelId, channelName, importance)
313
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
314
+ notificationManager.createNotificationChannel(channel)
315
+ }
316
+
317
+ // For Android 13+, check POST_NOTIFICATIONS permission
318
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
319
+ val granted = ContextCompat.checkSelfPermission(
320
+ context,
321
+ Manifest.permission.POST_NOTIFICATIONS
322
+ ) == PackageManager.PERMISSION_GRANTED
323
+
324
+ if (!granted) {
325
+ // Request permission - this will be handled by the app
326
+ appContext.currentActivity?.let { activity ->
327
+ ActivityCompat.requestPermissions(
328
+ activity,
329
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
330
+ 1001
331
+ )
332
+ }
333
+ }
334
+
335
+ return mapOf(
336
+ "granted" to granted,
337
+ "status" to if (granted) "granted" else "denied",
338
+ "canAskAgain" to true
339
+ )
340
+ }
341
+
342
+ // For older Android versions, notifications are allowed by default
343
+ return mapOf(
344
+ "granted" to true,
345
+ "status" to "granted",
346
+ "canAskAgain" to true
347
+ )
348
+ }
349
+
350
+ private fun getNotificationPermissions(): Map<String, Any> {
351
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
352
+ val granted = ContextCompat.checkSelfPermission(
353
+ context,
354
+ Manifest.permission.POST_NOTIFICATIONS
355
+ ) == PackageManager.PERMISSION_GRANTED
356
+
357
+ return mapOf(
358
+ "granted" to granted,
359
+ "status" to if (granted) "granted" else "denied",
360
+ "canAskAgain" to true
361
+ )
362
+ }
363
+
364
+ return mapOf(
365
+ "granted" to true,
366
+ "status" to "granted",
367
+ "canAskAgain" to true
368
+ )
369
+ }
370
+
371
+ // Helper Functions
372
+ private fun getAndroidId(): String? {
373
+ return try {
374
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
375
+ } catch (e: Exception) {
376
+ null
377
+ }
378
+ }
379
+
380
+ private fun getAppName(): String? {
381
+ return try {
382
+ val applicationInfo = context.applicationInfo
383
+ val stringId = applicationInfo.labelRes
384
+ if (stringId == 0) applicationInfo.nonLocalizedLabel?.toString()
385
+ else context.getString(stringId)
386
+ } catch (e: Exception) {
387
+ null
388
+ }
389
+ }
390
+
391
+ private fun getBuildNumber(packageInfo: android.content.pm.PackageInfo?): String? {
392
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
393
+ packageInfo?.longVersionCode?.toString()
394
+ } else {
395
+ @Suppress("DEPRECATION")
396
+ packageInfo?.versionCode?.toString()
397
+ }
398
+ }
399
+
400
+ private fun isEmulator(): Boolean {
401
+ return (Build.FINGERPRINT.startsWith("generic")
402
+ || Build.FINGERPRINT.startsWith("unknown")
403
+ || Build.MODEL.contains("google_sdk")
404
+ || Build.MODEL.contains("Emulator")
405
+ || Build.MODEL.contains("Android SDK built for x86")
406
+ || Build.MANUFACTURER.contains("Genymotion")
407
+ || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
408
+ || Build.PRODUCT == "google_sdk")
409
+ }
410
+
411
+ private fun getDisplayMetrics(): DisplayMetrics {
412
+ val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
413
+ val displayMetrics = DisplayMetrics()
414
+ @Suppress("DEPRECATION")
415
+ windowManager.defaultDisplay.getMetrics(displayMetrics)
416
+ return displayMetrics
417
+ }
418
+
419
+ private fun getInterfaceStyle(): String {
420
+ val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
421
+ return when (nightModeFlags) {
422
+ Configuration.UI_MODE_NIGHT_YES -> "dark"
423
+ Configuration.UI_MODE_NIGHT_NO -> "light"
424
+ else -> "unspecified"
425
+ }
426
+ }
427
+
428
+ private fun isLowPowerMode(): Boolean {
429
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
430
+ return powerManager?.isPowerSaveMode ?: false
431
+ }
432
+
433
+ private fun getTotalMemory(): Long {
434
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
435
+ val memoryInfo = ActivityManager.MemoryInfo()
436
+ activityManager.getMemoryInfo(memoryInfo)
437
+ return memoryInfo.totalMem
438
+ }
439
+
440
+ private fun getIso8601Timestamp(): String {
441
+ val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
442
+ sdf.timeZone = TimeZone.getTimeZone("UTC")
443
+ return sdf.format(Date())
444
+ }
445
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * RampKit Device Info Collector
3
+ * Collects device information using native modules for the /app-users endpoint
4
+ */
5
+ import { DeviceInfo } from "./types";
6
+ /**
7
+ * Get session start time
8
+ */
9
+ export declare function getSessionStartTime(): Date | null;
10
+ /**
11
+ * Get the current session duration in seconds
12
+ */
13
+ export declare function getSessionDurationSeconds(): number;
14
+ /**
15
+ * Collect all device information using native module
16
+ */
17
+ export declare function collectDeviceInfo(): Promise<DeviceInfo>;
18
+ /**
19
+ * Reset session (call when app is fully restarted)
20
+ */
21
+ export declare function resetSession(): void;