@usergist/feedback-react-native 0.1.0

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.
Files changed (242) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/UserGistFeedback.podspec +31 -0
  5. package/UserGistFeedbackExtension.podspec +23 -0
  6. package/android/build.gradle +92 -0
  7. package/android/consumer-rules.pro +5 -0
  8. package/android/src/main/AndroidManifest.xml +31 -0
  9. package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +153 -0
  10. package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +344 -0
  11. package/android/src/main/java/studio/usergist/feedback/UserGistPushPackage.kt +18 -0
  12. package/dist/NativeUserGistPush.d.ts +35 -0
  13. package/dist/NativeUserGistPush.d.ts.map +1 -0
  14. package/dist/NativeUserGistPush.js +13 -0
  15. package/dist/NativeUserGistPush.js.map +1 -0
  16. package/dist/Provider.d.ts +7 -0
  17. package/dist/Provider.d.ts.map +1 -0
  18. package/dist/Provider.js +300 -0
  19. package/dist/Provider.js.map +1 -0
  20. package/dist/Push.d.ts +103 -0
  21. package/dist/Push.d.ts.map +1 -0
  22. package/dist/Push.js +225 -0
  23. package/dist/Push.js.map +1 -0
  24. package/dist/UserGist.d.ts +210 -0
  25. package/dist/UserGist.d.ts.map +1 -0
  26. package/dist/UserGist.js +1246 -0
  27. package/dist/UserGist.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +4 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/internal/consent.d.ts +17 -0
  33. package/dist/internal/consent.d.ts.map +1 -0
  34. package/dist/internal/consent.js +98 -0
  35. package/dist/internal/consent.js.map +1 -0
  36. package/dist/internal/context.d.ts +12 -0
  37. package/dist/internal/context.d.ts.map +1 -0
  38. package/dist/internal/context.js +70 -0
  39. package/dist/internal/context.js.map +1 -0
  40. package/dist/internal/debug.d.ts +14 -0
  41. package/dist/internal/debug.d.ts.map +1 -0
  42. package/dist/internal/debug.js +73 -0
  43. package/dist/internal/debug.js.map +1 -0
  44. package/dist/internal/engine.d.ts +93 -0
  45. package/dist/internal/engine.d.ts.map +1 -0
  46. package/dist/internal/engine.js +904 -0
  47. package/dist/internal/engine.js.map +1 -0
  48. package/dist/internal/events.d.ts +49 -0
  49. package/dist/internal/events.d.ts.map +1 -0
  50. package/dist/internal/events.js +71 -0
  51. package/dist/internal/events.js.map +1 -0
  52. package/dist/internal/frequency-cap.d.ts +15 -0
  53. package/dist/internal/frequency-cap.d.ts.map +1 -0
  54. package/dist/internal/frequency-cap.js +84 -0
  55. package/dist/internal/frequency-cap.js.map +1 -0
  56. package/dist/internal/identity.d.ts +14 -0
  57. package/dist/internal/identity.d.ts.map +1 -0
  58. package/dist/internal/identity.js +113 -0
  59. package/dist/internal/identity.js.map +1 -0
  60. package/dist/internal/inapp-matcher.d.ts +13 -0
  61. package/dist/internal/inapp-matcher.d.ts.map +1 -0
  62. package/dist/internal/inapp-matcher.js +65 -0
  63. package/dist/internal/inapp-matcher.js.map +1 -0
  64. package/dist/internal/inapp-rules-cache.d.ts +16 -0
  65. package/dist/internal/inapp-rules-cache.d.ts.map +1 -0
  66. package/dist/internal/inapp-rules-cache.js +88 -0
  67. package/dist/internal/inapp-rules-cache.js.map +1 -0
  68. package/dist/internal/instruction-dedupe.d.ts +16 -0
  69. package/dist/internal/instruction-dedupe.d.ts.map +1 -0
  70. package/dist/internal/instruction-dedupe.js +52 -0
  71. package/dist/internal/instruction-dedupe.js.map +1 -0
  72. package/dist/internal/lifecycle.d.ts +12 -0
  73. package/dist/internal/lifecycle.d.ts.map +1 -0
  74. package/dist/internal/lifecycle.js +97 -0
  75. package/dist/internal/lifecycle.js.map +1 -0
  76. package/dist/internal/modal-coordinator.d.ts +4 -0
  77. package/dist/internal/modal-coordinator.d.ts.map +1 -0
  78. package/dist/internal/modal-coordinator.js +43 -0
  79. package/dist/internal/modal-coordinator.js.map +1 -0
  80. package/dist/internal/modal-queue.d.ts +9 -0
  81. package/dist/internal/modal-queue.d.ts.map +1 -0
  82. package/dist/internal/modal-queue.js +32 -0
  83. package/dist/internal/modal-queue.js.map +1 -0
  84. package/dist/internal/mutation-queue.d.ts +23 -0
  85. package/dist/internal/mutation-queue.d.ts.map +1 -0
  86. package/dist/internal/mutation-queue.js +66 -0
  87. package/dist/internal/mutation-queue.js.map +1 -0
  88. package/dist/internal/prompt-answers.d.ts +6 -0
  89. package/dist/internal/prompt-answers.d.ts.map +1 -0
  90. package/dist/internal/prompt-answers.js +10 -0
  91. package/dist/internal/prompt-answers.js.map +1 -0
  92. package/dist/internal/prompt-flow.d.ts +8 -0
  93. package/dist/internal/prompt-flow.d.ts.map +1 -0
  94. package/dist/internal/prompt-flow.js +26 -0
  95. package/dist/internal/prompt-flow.js.map +1 -0
  96. package/dist/internal/queue.d.ts +17 -0
  97. package/dist/internal/queue.d.ts.map +1 -0
  98. package/dist/internal/queue.js +172 -0
  99. package/dist/internal/queue.js.map +1 -0
  100. package/dist/internal/requests.d.ts +22 -0
  101. package/dist/internal/requests.d.ts.map +1 -0
  102. package/dist/internal/requests.js +146 -0
  103. package/dist/internal/requests.js.map +1 -0
  104. package/dist/internal/rules-cache.d.ts +16 -0
  105. package/dist/internal/rules-cache.d.ts.map +1 -0
  106. package/dist/internal/rules-cache.js +86 -0
  107. package/dist/internal/rules-cache.js.map +1 -0
  108. package/dist/internal/storage.d.ts +36 -0
  109. package/dist/internal/storage.d.ts.map +1 -0
  110. package/dist/internal/storage.js +213 -0
  111. package/dist/internal/storage.js.map +1 -0
  112. package/dist/internal/survey-completion.d.ts +10 -0
  113. package/dist/internal/survey-completion.d.ts.map +1 -0
  114. package/dist/internal/survey-completion.js +11 -0
  115. package/dist/internal/survey-completion.js.map +1 -0
  116. package/dist/internal/survey-matcher.d.ts +21 -0
  117. package/dist/internal/survey-matcher.d.ts.map +1 -0
  118. package/dist/internal/survey-matcher.js +139 -0
  119. package/dist/internal/survey-matcher.js.map +1 -0
  120. package/dist/internal/survey-rules-cache.d.ts +17 -0
  121. package/dist/internal/survey-rules-cache.d.ts.map +1 -0
  122. package/dist/internal/survey-rules-cache.js +91 -0
  123. package/dist/internal/survey-rules-cache.js.map +1 -0
  124. package/dist/internal/survey-store.d.ts +19 -0
  125. package/dist/internal/survey-store.d.ts.map +1 -0
  126. package/dist/internal/survey-store.js +59 -0
  127. package/dist/internal/survey-store.js.map +1 -0
  128. package/dist/internal/transport.d.ts +210 -0
  129. package/dist/internal/transport.d.ts.map +1 -0
  130. package/dist/internal/transport.js +498 -0
  131. package/dist/internal/transport.js.map +1 -0
  132. package/dist/internal/trigger-matcher.d.ts +21 -0
  133. package/dist/internal/trigger-matcher.d.ts.map +1 -0
  134. package/dist/internal/trigger-matcher.js +114 -0
  135. package/dist/internal/trigger-matcher.js.map +1 -0
  136. package/dist/internal/types.d.ts +89 -0
  137. package/dist/internal/types.d.ts.map +1 -0
  138. package/dist/internal/types.js +2 -0
  139. package/dist/internal/types.js.map +1 -0
  140. package/dist/internal/user-state.d.ts +11 -0
  141. package/dist/internal/user-state.d.ts.map +1 -0
  142. package/dist/internal/user-state.js +118 -0
  143. package/dist/internal/user-state.js.map +1 -0
  144. package/dist/native/events.d.ts +27 -0
  145. package/dist/native/events.d.ts.map +1 -0
  146. package/dist/native/events.js +12 -0
  147. package/dist/native/events.js.map +1 -0
  148. package/dist/native/push-bridge.d.ts +20 -0
  149. package/dist/native/push-bridge.d.ts.map +1 -0
  150. package/dist/native/push-bridge.js +92 -0
  151. package/dist/native/push-bridge.js.map +1 -0
  152. package/dist/preview/PromptShell.d.ts +9 -0
  153. package/dist/preview/PromptShell.d.ts.map +1 -0
  154. package/dist/preview/PromptShell.js +128 -0
  155. package/dist/preview/PromptShell.js.map +1 -0
  156. package/dist/preview/SurveyShell.d.ts +15 -0
  157. package/dist/preview/SurveyShell.d.ts.map +1 -0
  158. package/dist/preview/SurveyShell.js +197 -0
  159. package/dist/preview/SurveyShell.js.map +1 -0
  160. package/dist/preview/index.d.ts +4 -0
  161. package/dist/preview/index.d.ts.map +1 -0
  162. package/dist/preview/index.js +16 -0
  163. package/dist/preview/index.js.map +1 -0
  164. package/dist/ui/InAppMessageView.d.ts +12 -0
  165. package/dist/ui/InAppMessageView.d.ts.map +1 -0
  166. package/dist/ui/InAppMessageView.js +260 -0
  167. package/dist/ui/InAppMessageView.js.map +1 -0
  168. package/dist/ui/PromptSheet.d.ts +12 -0
  169. package/dist/ui/PromptSheet.d.ts.map +1 -0
  170. package/dist/ui/PromptSheet.js +285 -0
  171. package/dist/ui/PromptSheet.js.map +1 -0
  172. package/dist/ui/QuestionImageHeader.d.ts +12 -0
  173. package/dist/ui/QuestionImageHeader.d.ts.map +1 -0
  174. package/dist/ui/QuestionImageHeader.js +22 -0
  175. package/dist/ui/QuestionImageHeader.js.map +1 -0
  176. package/dist/ui/SurveyView.d.ts +25 -0
  177. package/dist/ui/SurveyView.d.ts.map +1 -0
  178. package/dist/ui/SurveyView.js +478 -0
  179. package/dist/ui/SurveyView.js.map +1 -0
  180. package/dist/ui/animations.d.ts +4 -0
  181. package/dist/ui/animations.d.ts.map +1 -0
  182. package/dist/ui/animations.js +24 -0
  183. package/dist/ui/animations.js.map +1 -0
  184. package/dist/ui/questions/MultipleChoiceQuestion.d.ts +12 -0
  185. package/dist/ui/questions/MultipleChoiceQuestion.d.ts.map +1 -0
  186. package/dist/ui/questions/MultipleChoiceQuestion.js +41 -0
  187. package/dist/ui/questions/MultipleChoiceQuestion.js.map +1 -0
  188. package/dist/ui/questions/NpsQuestion.d.ts +14 -0
  189. package/dist/ui/questions/NpsQuestion.d.ts.map +1 -0
  190. package/dist/ui/questions/NpsQuestion.js +73 -0
  191. package/dist/ui/questions/NpsQuestion.js.map +1 -0
  192. package/dist/ui/questions/RatingQuestion.d.ts +21 -0
  193. package/dist/ui/questions/RatingQuestion.d.ts.map +1 -0
  194. package/dist/ui/questions/RatingQuestion.js +91 -0
  195. package/dist/ui/questions/RatingQuestion.js.map +1 -0
  196. package/dist/ui/questions/ShortTextQuestion.d.ts +12 -0
  197. package/dist/ui/questions/ShortTextQuestion.d.ts.map +1 -0
  198. package/dist/ui/questions/ShortTextQuestion.js +25 -0
  199. package/dist/ui/questions/ShortTextQuestion.js.map +1 -0
  200. package/dist/ui/requests/BoardView.d.ts +9 -0
  201. package/dist/ui/requests/BoardView.d.ts.map +1 -0
  202. package/dist/ui/requests/BoardView.js +270 -0
  203. package/dist/ui/requests/BoardView.js.map +1 -0
  204. package/dist/ui/requests/CommentsSection.d.ts +7 -0
  205. package/dist/ui/requests/CommentsSection.d.ts.map +1 -0
  206. package/dist/ui/requests/CommentsSection.js +254 -0
  207. package/dist/ui/requests/CommentsSection.js.map +1 -0
  208. package/dist/ui/requests/DetailView.d.ts +8 -0
  209. package/dist/ui/requests/DetailView.d.ts.map +1 -0
  210. package/dist/ui/requests/DetailView.js +206 -0
  211. package/dist/ui/requests/DetailView.js.map +1 -0
  212. package/dist/ui/requests/RequestsHost.d.ts +12 -0
  213. package/dist/ui/requests/RequestsHost.d.ts.map +1 -0
  214. package/dist/ui/requests/RequestsHost.js +58 -0
  215. package/dist/ui/requests/RequestsHost.js.map +1 -0
  216. package/dist/ui/requests/StatusPill.d.ts +6 -0
  217. package/dist/ui/requests/StatusPill.d.ts.map +1 -0
  218. package/dist/ui/requests/StatusPill.js +25 -0
  219. package/dist/ui/requests/StatusPill.js.map +1 -0
  220. package/dist/ui/requests/SubmitView.d.ts +8 -0
  221. package/dist/ui/requests/SubmitView.d.ts.map +1 -0
  222. package/dist/ui/requests/SubmitView.js +94 -0
  223. package/dist/ui/requests/SubmitView.js.map +1 -0
  224. package/dist/ui/requests/shared.d.ts +21 -0
  225. package/dist/ui/requests/shared.d.ts.map +1 -0
  226. package/dist/ui/requests/shared.js +74 -0
  227. package/dist/ui/requests/shared.js.map +1 -0
  228. package/dist/ui/survey-questions.d.ts +20 -0
  229. package/dist/ui/survey-questions.d.ts.map +1 -0
  230. package/dist/ui/survey-questions.js +211 -0
  231. package/dist/ui/survey-questions.js.map +1 -0
  232. package/dist/ui/theme.d.ts +15 -0
  233. package/dist/ui/theme.d.ts.map +1 -0
  234. package/dist/ui/theme.js +31 -0
  235. package/dist/ui/theme.js.map +1 -0
  236. package/ios/Extension/UserGistNotificationService.swift +292 -0
  237. package/ios/UserGistPush.h +16 -0
  238. package/ios/UserGistPush.mm +158 -0
  239. package/ios/UserGistPushImpl.swift +399 -0
  240. package/ios/UserGistPushSwizzle.m +168 -0
  241. package/package.json +89 -0
  242. package/react-native.config.js +16 -0
@@ -0,0 +1,344 @@
1
+ package studio.usergist.feedback
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.Intent
6
+ import android.content.pm.PackageManager
7
+ import android.os.Build
8
+ import androidx.core.app.ActivityCompat
9
+ import androidx.core.app.NotificationManagerCompat
10
+ import androidx.security.crypto.EncryptedSharedPreferences
11
+ import androidx.security.crypto.MasterKey
12
+ import com.facebook.react.bridge.Arguments
13
+ import com.facebook.react.bridge.Promise
14
+ import com.facebook.react.bridge.ReactApplicationContext
15
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
16
+ import com.facebook.react.bridge.ReactMethod
17
+ import com.facebook.react.bridge.ReadableMap
18
+ import com.facebook.react.bridge.ReadableArray
19
+ import com.facebook.react.bridge.WritableMap
20
+ import com.facebook.react.bridge.ActivityEventListener
21
+ import com.facebook.react.bridge.LifecycleEventListener
22
+ import com.facebook.react.modules.core.PermissionAwareActivity
23
+ import com.facebook.react.modules.core.PermissionListener
24
+ import com.facebook.react.modules.core.DeviceEventManagerModule
25
+ import com.google.android.gms.tasks.OnCompleteListener
26
+ import com.google.firebase.messaging.FirebaseMessaging
27
+
28
+ /**
29
+ * Native bridge for the UserGist push pipeline on Android.
30
+ *
31
+ * Responsibilities:
32
+ * - Request POST_NOTIFICATIONS at runtime on Android 13+.
33
+ * - Surface the current FCM token via FirebaseMessaging.getInstance().token.
34
+ * - Forward token-refresh + RemoteMessage delivery to JS. The actual
35
+ * RemoteMessage handling lives in UserGistFirebaseMessagingService;
36
+ * this module exposes only the imperative side (JS -> native calls).
37
+ */
38
+ @Suppress("UNUSED_PARAMETER")
39
+ class UserGistPushModule(reactContext: ReactApplicationContext) :
40
+ ReactContextBaseJavaModule(reactContext), LifecycleEventListener, ActivityEventListener {
41
+
42
+ init {
43
+ // Wire ourselves into the static event-emit helper so the Firebase
44
+ // service can hand events back without holding a strong reference to
45
+ // the React module.
46
+ UserGistPushEventBus.attach(reactContext)
47
+ reactContext.addLifecycleEventListener(this)
48
+ reactContext.addActivityEventListener(this)
49
+ }
50
+
51
+ override fun getName(): String = NAME
52
+
53
+ private val securePreferences by lazy {
54
+ val masterKey = MasterKey.Builder(reactApplicationContext)
55
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
56
+ .build()
57
+ EncryptedSharedPreferences.create(
58
+ reactApplicationContext,
59
+ "usergist_react_native_secure",
60
+ masterKey,
61
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
62
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
63
+ )
64
+ }
65
+
66
+ override fun onHostResume() {
67
+ currentActivity?.intent?.let { consumeNotificationIntent(it) }
68
+ }
69
+
70
+ override fun onHostPause() = Unit
71
+ override fun onHostDestroy() = Unit
72
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) = Unit
73
+
74
+ override fun onNewIntent(intent: Intent) {
75
+ consumeNotificationIntent(intent)
76
+ }
77
+
78
+ private fun consumeNotificationIntent(intent: Intent) {
79
+ val extras = intent.extras ?: return
80
+ val keys = extras.keySet().filter { it.startsWith("usergist_") }
81
+ if (keys.isEmpty()) return
82
+ val body = Arguments.createMap()
83
+ val data = Arguments.createMap()
84
+ for (key in keys) {
85
+ val value = extras.getString(key) ?: continue
86
+ val normalized = key.removePrefix("usergist_")
87
+ when (normalized) {
88
+ "title" -> body.putString("title", value)
89
+ "body" -> body.putString("body", value)
90
+ "delivery_id" -> body.putString("deliveryId", value)
91
+ "action_identifier" -> body.putString("actionIdentifier", value)
92
+ else -> data.putString(key, value)
93
+ }
94
+ intent.removeExtra(key)
95
+ }
96
+ body.putMap("data", data)
97
+ UserGistPushEventBus.emitNotificationOpened(body)
98
+ }
99
+
100
+ // MARK: - Exported methods
101
+
102
+ @ReactMethod
103
+ fun enablePush(_options: ReadableMap?, promise: Promise) {
104
+ // Android < 13: notification permission is install-time, no runtime
105
+ // prompt. We can short-circuit straight to the token fetch.
106
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
107
+ val areEnabled = NotificationManagerCompat.from(reactApplicationContext).areNotificationsEnabled()
108
+ if (!areEnabled) {
109
+ promise.resolve(buildResult(granted = false, status = "denied", token = null))
110
+ return
111
+ }
112
+ fetchTokenAndResolve(promise, status = "authorized")
113
+ return
114
+ }
115
+ val activity = currentActivity
116
+ if (activity == null) {
117
+ // No activity: can't ask for permission. Resolve with current state.
118
+ val granted = ActivityCompat.checkSelfPermission(
119
+ reactApplicationContext,
120
+ Manifest.permission.POST_NOTIFICATIONS
121
+ ) == PackageManager.PERMISSION_GRANTED
122
+ if (granted) {
123
+ fetchTokenAndResolve(promise, status = "authorized")
124
+ } else {
125
+ promise.resolve(buildResult(granted = false, status = "not_determined", token = null))
126
+ }
127
+ return
128
+ }
129
+ val already = ActivityCompat.checkSelfPermission(
130
+ activity,
131
+ Manifest.permission.POST_NOTIFICATIONS
132
+ ) == PackageManager.PERMISSION_GRANTED
133
+ if (already) {
134
+ fetchTokenAndResolve(promise, status = "authorized")
135
+ return
136
+ }
137
+ val permAware = activity as? PermissionAwareActivity
138
+ if (permAware == null) {
139
+ // Fallback: classic ActivityCompat, no result callback we can hook.
140
+ ActivityCompat.requestPermissions(
141
+ activity,
142
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
143
+ REQUEST_CODE
144
+ )
145
+ // Without a callback we can't know the outcome — resolve as
146
+ // not_determined so the caller can re-call after the dialog closes.
147
+ promise.resolve(buildResult(granted = false, status = "not_determined", token = null))
148
+ return
149
+ }
150
+ permAware.requestPermissions(
151
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
152
+ REQUEST_CODE,
153
+ PermissionListener { _, _, grantResults ->
154
+ val grantedNow = grantResults.isNotEmpty() &&
155
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
156
+ if (grantedNow) {
157
+ fetchTokenAndResolve(promise, status = "authorized")
158
+ } else {
159
+ promise.resolve(buildResult(granted = false, status = "denied", token = null))
160
+ }
161
+ true
162
+ }
163
+ )
164
+ }
165
+
166
+ @ReactMethod
167
+ fun disablePush(promise: Promise) {
168
+ FirebaseMessaging.getInstance().deleteToken()
169
+ .addOnCompleteListener { promise.resolve(null) }
170
+ }
171
+
172
+ @ReactMethod
173
+ fun getPushPermissionStatus(promise: Promise) {
174
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
175
+ val areEnabled = NotificationManagerCompat.from(reactApplicationContext).areNotificationsEnabled()
176
+ promise.resolve(if (areEnabled) "authorized" else "denied")
177
+ return
178
+ }
179
+ val granted = ActivityCompat.checkSelfPermission(
180
+ reactApplicationContext,
181
+ Manifest.permission.POST_NOTIFICATIONS
182
+ ) == PackageManager.PERMISSION_GRANTED
183
+ promise.resolve(if (granted) "authorized" else "denied")
184
+ }
185
+
186
+ @ReactMethod
187
+ fun setBadgeCount(_count: Double, promise: Promise) {
188
+ // Android does not expose a portable badge API; some launchers honour
189
+ // a ShortcutBadger-style broadcast but it's launcher-specific. No-op
190
+ // for now; consumers who need it can use ShortcutBadger directly.
191
+ promise.resolve(null)
192
+ }
193
+
194
+ @ReactMethod
195
+ fun getInitialNotification(promise: Promise) {
196
+ promise.resolve(UserGistPushEventBus.takeInitialNotification())
197
+ }
198
+
199
+ @ReactMethod
200
+ fun secureGetItem(key: String, promise: Promise) {
201
+ runCatching { securePreferences.getString(key, null) }
202
+ .onSuccess(promise::resolve)
203
+ .onFailure { promise.reject("secure_read_failed", it) }
204
+ }
205
+
206
+ @ReactMethod
207
+ fun secureSetItem(key: String, value: String, promise: Promise) {
208
+ runCatching {
209
+ check(securePreferences.edit().putString(key, value).commit())
210
+ }.onSuccess { promise.resolve(null) }
211
+ .onFailure { promise.reject("secure_write_failed", it) }
212
+ }
213
+
214
+ @ReactMethod
215
+ fun secureRemoveItem(key: String, promise: Promise) {
216
+ runCatching { check(securePreferences.edit().remove(key).commit()) }
217
+ .onSuccess { promise.resolve(null) }
218
+ .onFailure { promise.reject("secure_remove_failed", it) }
219
+ }
220
+
221
+ @ReactMethod
222
+ fun secureMultiRemove(keys: ReadableArray, promise: Promise) {
223
+ runCatching {
224
+ val edit = securePreferences.edit()
225
+ for (index in 0 until keys.size()) {
226
+ edit.remove(keys.getString(index))
227
+ }
228
+ check(edit.commit())
229
+ }.onSuccess { promise.resolve(null) }
230
+ .onFailure { promise.reject("secure_remove_failed", it) }
231
+ }
232
+
233
+ // Required for RN's NativeEventEmitter API even though we emit from a
234
+ // separate static bus — RN will warn otherwise.
235
+ @ReactMethod
236
+ fun addListener(_eventName: String?) { /* no-op */ }
237
+
238
+ @ReactMethod
239
+ fun removeListeners(_count: Double) { /* no-op */ }
240
+
241
+ // MARK: - Internal
242
+
243
+ private fun fetchTokenAndResolve(promise: Promise, status: String) {
244
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(
245
+ OnCompleteListener { task ->
246
+ if (!task.isSuccessful) {
247
+ val msg = task.exception?.localizedMessage ?: "fcm_token_unavailable"
248
+ promise.resolve(buildResult(granted = true, status = status, token = null, error = msg))
249
+ return@OnCompleteListener
250
+ }
251
+ val token = task.result
252
+ // Emit on top of resolving so JS gets both the resolution AND the
253
+ // refresh-style event (idempotent on the server side).
254
+ UserGistPushEventBus.emitToken(token)
255
+ promise.resolve(buildResult(granted = true, status = status, token = token))
256
+ }
257
+ )
258
+ }
259
+
260
+ private fun buildResult(
261
+ granted: Boolean,
262
+ status: String,
263
+ token: String?,
264
+ error: String? = null
265
+ ): WritableMap {
266
+ val map = Arguments.createMap()
267
+ map.putBoolean("granted", granted)
268
+ map.putString("status", status)
269
+ map.putString("platform", "android")
270
+ if (token != null) map.putString("token", token)
271
+ if (error != null) map.putString("error", error)
272
+ return map
273
+ }
274
+
275
+ companion object {
276
+ const val NAME = "UserGistPush"
277
+ private const val REQUEST_CODE = 0x52A1 // arbitrary 16-bit; "RA" in hex
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Static bus owned by the UserGistFirebaseMessagingService; lets the service
283
+ * forward events to JS without holding a strong reference to the
284
+ * (possibly-not-yet-initialized) React Native bridge.
285
+ */
286
+ internal object UserGistPushEventBus {
287
+ @Volatile private var reactContext: ReactApplicationContext? = null
288
+ private val pending: MutableList<Pair<String, WritableMap>> = mutableListOf()
289
+ private var initialNotification: WritableMap? = null
290
+
291
+ @Synchronized
292
+ fun attach(ctx: ReactApplicationContext) {
293
+ this.reactContext = ctx
294
+ val drained = pending.toList()
295
+ pending.clear()
296
+ for ((name, body) in drained) {
297
+ send(name, body)
298
+ }
299
+ }
300
+
301
+ fun emitToken(token: String) {
302
+ val map = Arguments.createMap()
303
+ map.putString("token", token)
304
+ map.putString("platform", "android")
305
+ emit("UserGistPush:tokenReceived", map)
306
+ }
307
+
308
+ fun emitNotificationReceived(body: WritableMap) {
309
+ emit("UserGistPush:notificationReceived", body)
310
+ }
311
+
312
+ @Synchronized
313
+ fun emitNotificationOpened(body: WritableMap) {
314
+ if (initialNotification == null) {
315
+ // Snapshot so getInitialNotification() can return it once.
316
+ initialNotification = Arguments.createMap().apply { merge(body) }
317
+ }
318
+ emit("UserGistPush:notificationOpened", body)
319
+ }
320
+
321
+ @Synchronized
322
+ fun takeInitialNotification(): WritableMap? {
323
+ val v = initialNotification
324
+ initialNotification = null
325
+ return v
326
+ }
327
+
328
+ @Synchronized
329
+ private fun emit(name: String, body: WritableMap) {
330
+ val ctx = reactContext
331
+ if (ctx == null || !ctx.hasActiveReactInstance()) {
332
+ if (pending.size >= 100) pending.removeAt(0)
333
+ pending.add(name to body)
334
+ return
335
+ }
336
+ send(name, body)
337
+ }
338
+
339
+ private fun send(name: String, body: WritableMap) {
340
+ val ctx = reactContext ?: return
341
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
342
+ .emit(name, body)
343
+ }
344
+ }
@@ -0,0 +1,18 @@
1
+ package studio.usergist.feedback
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ /**
9
+ * Registers UserGistPushModule with the React Native bridge. Picked up via
10
+ * autolinking — see react-native.config.js at the package root.
11
+ */
12
+ class UserGistPushPackage : ReactPackage {
13
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
14
+ listOf(UserGistPushModule(reactContext))
15
+
16
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
17
+ emptyList()
18
+ }
@@ -0,0 +1,35 @@
1
+ import type { TurboModule } from 'react-native';
2
+ export interface EnablePushOptions {
3
+ readonly skipPermissionRequestIfDenied?: boolean;
4
+ readonly environment?: 'sandbox' | 'production';
5
+ /** Disable delegate interception when the host forwards tokens/messages via Push. */
6
+ readonly installDelegateProxy?: boolean;
7
+ }
8
+ export interface EnablePushResult {
9
+ readonly granted: boolean;
10
+ readonly status: 'authorized' | 'provisional' | 'denied' | 'not_determined';
11
+ readonly token?: string;
12
+ readonly platform: 'ios' | 'android';
13
+ readonly error?: string;
14
+ }
15
+ export type PushPermissionStatus = 'authorized' | 'provisional' | 'denied' | 'not_determined';
16
+ export interface Spec extends TurboModule {
17
+ enablePush(options: {
18
+ [key: string]: unknown;
19
+ } | null): Promise<EnablePushResult>;
20
+ disablePush(): Promise<void>;
21
+ getPushPermissionStatus(): Promise<PushPermissionStatus>;
22
+ setBadgeCount(count: number): Promise<void>;
23
+ getInitialNotification(): Promise<{
24
+ [key: string]: unknown;
25
+ } | null>;
26
+ secureGetItem(key: string): Promise<string | null>;
27
+ secureSetItem(key: string, value: string): Promise<void>;
28
+ secureRemoveItem(key: string): Promise<void>;
29
+ secureMultiRemove(keys: ReadonlyArray<string>): Promise<void>;
30
+ addListener(eventName: string): void;
31
+ removeListeners(count: number): void;
32
+ }
33
+ declare const _default: Spec | null;
34
+ export default _default;
35
+ //# sourceMappingURL=NativeUserGistPush.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeUserGistPush.d.ts","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAG/C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,6BAA6B,CAAC,EAAE,OAAO,CAAA;IAChD,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAA;IAC/C,qFAAqF;IACrF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,QAAQ,GAAG,gBAAgB,CAAA;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,oBAAoB,GAC5B,YAAY,GACZ,aAAa,GACb,QAAQ,GACR,gBAAgB,CAAA;AAEpB,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,UAAU,CAAC,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACjF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,uBAAuB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACxD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,sBAAsB,IAAI,OAAO,CAAC;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpE,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAClD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE7D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrC;;AAED,wBAA4D"}
@@ -0,0 +1,13 @@
1
+ // TurboModule spec for the UserGist push native module.
2
+ //
3
+ // React Native's codegen consumes this file and generates the native
4
+ // scaffolding that lets the new architecture (Bridgeless / Fabric)
5
+ // resolve our module without going through the legacy bridge.
6
+ //
7
+ // At runtime, `src/native/push-bridge.ts` first tries
8
+ // `TurboModuleRegistry.getEnforcing<Spec>('UserGistPush')` and falls back
9
+ // to `NativeModules.UserGistPush` so the package works in both
10
+ // architectures (RN 0.72 → 0.79+).
11
+ import { TurboModuleRegistry } from 'react-native';
12
+ export default TurboModuleRegistry.get('UserGistPush');
13
+ //# sourceMappingURL=NativeUserGistPush.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeUserGistPush.js","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,8DAA8D;AAC9D,EAAE;AACF,sDAAsD;AACtD,0EAA0E;AAC1E,+DAA+D;AAC/D,mCAAmC;AAGnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAsClD,eAAe,mBAAmB,CAAC,GAAG,CAAO,cAAc,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ readonly children?: React.ReactNode;
4
+ }
5
+ export declare function UserGistProvider({ children }: Props): React.ReactElement;
6
+ export {};
7
+ //# sourceMappingURL=Provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Provider.d.ts","sourceRoot":"","sources":["../src/Provider.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAmD,MAAM,OAAO,CAAA;AA0EvE,UAAU,KAAK;IACb,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACpC;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,YAAY,CAoTxE"}