react-native-carrot-quest 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 (121) hide show
  1. package/CarrotQuest.podspec +34 -0
  2. package/LICENSE +20 -0
  3. package/README.md +457 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +136 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuest.kt +585 -0
  9. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestPackage.kt +22 -0
  10. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestSetup.kt +472 -0
  11. package/ios/CarrotQuest.swift +588 -0
  12. package/ios/CarrotQuestSetup.swift +333 -0
  13. package/lib/module/CarrotQuest.nitro.js +4 -0
  14. package/lib/module/CarrotQuest.nitro.js.map +1 -0
  15. package/lib/module/carrot-quest.js +136 -0
  16. package/lib/module/carrot-quest.js.map +1 -0
  17. package/lib/module/carrot-quest.native.js +155 -0
  18. package/lib/module/carrot-quest.native.js.map +1 -0
  19. package/lib/module/index.js +6 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/subscriptions.js +76 -0
  23. package/lib/module/subscriptions.js.map +1 -0
  24. package/lib/typescript/package.json +1 -0
  25. package/lib/typescript/src/CarrotQuest.nitro.d.ts +394 -0
  26. package/lib/typescript/src/CarrotQuest.nitro.d.ts.map +1 -0
  27. package/lib/typescript/src/carrot-quest.d.ts +37 -0
  28. package/lib/typescript/src/carrot-quest.d.ts.map +1 -0
  29. package/lib/typescript/src/carrot-quest.native.d.ts +37 -0
  30. package/lib/typescript/src/carrot-quest.native.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +6 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -0
  33. package/lib/typescript/src/subscriptions.d.ts +32 -0
  34. package/lib/typescript/src/subscriptions.d.ts.map +1 -0
  35. package/nitro.json +23 -0
  36. package/nitrogen/generated/android/c++/JCarrotQuestEventValue.cpp +30 -0
  37. package/nitrogen/generated/android/c++/JCarrotQuestEventValue.hpp +83 -0
  38. package/nitrogen/generated/android/c++/JCarrotQuestLogCategory.hpp +76 -0
  39. package/nitrogen/generated/android/c++/JCarrotQuestLogEntry.hpp +100 -0
  40. package/nitrogen/generated/android/c++/JCarrotQuestLogLevel.hpp +70 -0
  41. package/nitrogen/generated/android/c++/JCarrotQuestProperty.hpp +74 -0
  42. package/nitrogen/generated/android/c++/JCarrotQuestPropertyKind.hpp +61 -0
  43. package/nitrogen/generated/android/c++/JCarrotQuestPropertyOperation.hpp +73 -0
  44. package/nitrogen/generated/android/c++/JCarrotQuestSetupOptions.hpp +94 -0
  45. package/nitrogen/generated/android/c++/JCarrotQuestTheme.hpp +64 -0
  46. package/nitrogen/generated/android/c++/JCarrotQuestUrlSource.hpp +64 -0
  47. package/nitrogen/generated/android/c++/JCarrotQuestUserProperties.hpp +84 -0
  48. package/nitrogen/generated/android/c++/JFunc_void_CarrotQuestLogEntry.hpp +84 -0
  49. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  52. package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.cpp +388 -0
  53. package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.hpp +96 -0
  54. package/nitrogen/generated/android/carrotquest+autolinking.cmake +82 -0
  55. package/nitrogen/generated/android/carrotquest+autolinking.gradle +27 -0
  56. package/nitrogen/generated/android/carrotquestOnLoad.cpp +62 -0
  57. package/nitrogen/generated/android/carrotquestOnLoad.hpp +34 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestEventValue.kt +75 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogCategory.kt +29 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogEntry.kt +81 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogLevel.kt +27 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestProperty.kt +66 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyKind.kt +24 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyOperation.kt +28 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestSetupOptions.kt +91 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestTheme.kt +25 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUrlSource.kt +25 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUserProperties.kt +66 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_CarrotQuestLogEntry.kt +80 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_bool.kt +80 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_double.kt +80 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_std__string.kt +80 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/HybridCarrotQuestSpec.kt +209 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/carrotquestOnLoad.kt +35 -0
  75. package/nitrogen/generated/ios/CarrotQuest+autolinking.rb +62 -0
  76. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.cpp +97 -0
  77. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.hpp +637 -0
  78. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Umbrella.hpp +80 -0
  79. package/nitrogen/generated/ios/CarrotQuestAutolinking.mm +33 -0
  80. package/nitrogen/generated/ios/CarrotQuestAutolinking.swift +26 -0
  81. package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.cpp +11 -0
  82. package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.hpp +344 -0
  83. package/nitrogen/generated/ios/swift/CarrotQuestEventValue.swift +32 -0
  84. package/nitrogen/generated/ios/swift/CarrotQuestLogCategory.swift +64 -0
  85. package/nitrogen/generated/ios/swift/CarrotQuestLogEntry.swift +86 -0
  86. package/nitrogen/generated/ios/swift/CarrotQuestLogLevel.swift +56 -0
  87. package/nitrogen/generated/ios/swift/CarrotQuestProperty.swift +56 -0
  88. package/nitrogen/generated/ios/swift/CarrotQuestPropertyKind.swift +44 -0
  89. package/nitrogen/generated/ios/swift/CarrotQuestPropertyOperation.swift +60 -0
  90. package/nitrogen/generated/ios/swift/CarrotQuestSetupOptions.swift +172 -0
  91. package/nitrogen/generated/ios/swift/CarrotQuestTheme.swift +48 -0
  92. package/nitrogen/generated/ios/swift/CarrotQuestUrlSource.swift +48 -0
  93. package/nitrogen/generated/ios/swift/CarrotQuestUserProperties.swift +110 -0
  94. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  95. package/nitrogen/generated/ios/swift/Func_void_CarrotQuestLogEntry.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  97. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  98. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  99. package/nitrogen/generated/ios/swift/Func_void_std__optional_double_.swift +53 -0
  100. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
  101. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  102. package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec.swift +88 -0
  103. package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec_cxx.swift +674 -0
  104. package/nitrogen/generated/shared/c++/CarrotQuestLogCategory.hpp +100 -0
  105. package/nitrogen/generated/shared/c++/CarrotQuestLogEntry.hpp +114 -0
  106. package/nitrogen/generated/shared/c++/CarrotQuestLogLevel.hpp +92 -0
  107. package/nitrogen/generated/shared/c++/CarrotQuestProperty.hpp +101 -0
  108. package/nitrogen/generated/shared/c++/CarrotQuestPropertyKind.hpp +80 -0
  109. package/nitrogen/generated/shared/c++/CarrotQuestPropertyOperation.hpp +96 -0
  110. package/nitrogen/generated/shared/c++/CarrotQuestSetupOptions.hpp +121 -0
  111. package/nitrogen/generated/shared/c++/CarrotQuestTheme.hpp +84 -0
  112. package/nitrogen/generated/shared/c++/CarrotQuestUrlSource.hpp +84 -0
  113. package/nitrogen/generated/shared/c++/CarrotQuestUserProperties.hpp +97 -0
  114. package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.cpp +55 -0
  115. package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.hpp +118 -0
  116. package/package.json +140 -0
  117. package/src/CarrotQuest.nitro.ts +493 -0
  118. package/src/carrot-quest.native.tsx +241 -0
  119. package/src/carrot-quest.tsx +207 -0
  120. package/src/index.tsx +57 -0
  121. package/src/subscriptions.ts +83 -0
@@ -0,0 +1,472 @@
1
+ package com.margelo.nitro.carrotquest
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.util.Log
6
+ import io.carrotquest_sdk.android.Callback
7
+ import io.carrotquest_sdk.android.Carrot
8
+ import io.carrotquest_sdk.android.core.logging.SdkLogLevel
9
+ import io.carrotquest_sdk.android.core.main.ThemeSdk
10
+ import java.util.Locale
11
+
12
+ /**
13
+ * Native entry point for initialising the Carrot quest SDK at app launch.
14
+ *
15
+ * Call this from `Application.onCreate` when you want the SDK ready before the
16
+ * JS bundle finishes loading. Apps that do not care about launch timing can
17
+ * skip it and call `setup()` from JS instead.
18
+ *
19
+ * ```kotlin
20
+ * // MainApplication.kt
21
+ * CarrotQuestSetup.configure(this, apiKey = "your-key", locale = "ru") { error ->
22
+ * if (error != null) Log.e("App", "Carrot setup failed", error)
23
+ * }
24
+ * ```
25
+ *
26
+ * Initialisation is single-flight: concurrent calls that agree on the API key
27
+ * and the account-critical options join the in-flight attempt instead of
28
+ * starting a second one, and every caller is notified when it settles. Calls
29
+ * that *disagree* fail with [CarrotQuestConfigurationConflictException] rather
30
+ * than silently keeping the first configuration.
31
+ *
32
+ * `Carrot.setup` performs I/O, so call this off the main thread; this object
33
+ * does not spawn its own thread, leaving the scheduler choice to the caller.
34
+ */
35
+ object CarrotQuestSetup {
36
+ private const val TAG = "CarrotQuest"
37
+
38
+ /**
39
+ * The options that decide *which account and server* the SDK talks to.
40
+ *
41
+ * Everything else (theme, parent activity, notification icon, log level) is
42
+ * applied after initialisation and can be changed freely, so it is not part
43
+ * of the conflict check.
44
+ */
45
+ private data class Identity(
46
+ val apiKey: String,
47
+ val locale: String?,
48
+ val useEuServer: Boolean?,
49
+ )
50
+
51
+ private sealed interface State {
52
+ object Idle : State
53
+ data class Initializing(val identity: Identity) : State
54
+ data class Configured(val identity: Identity) : State
55
+ }
56
+
57
+ private val lock = Any()
58
+ private var state: State = State.Idle
59
+ private var pending = mutableListOf<(Throwable?) -> Unit>()
60
+ /** SDK operations called after setup started but before it finished. */
61
+ private var readinessWaiters = mutableListOf<(Throwable?) -> Unit>()
62
+
63
+ /** Application context the SDK was initialised with. Used to re-init after logout. */
64
+ @Volatile
65
+ private var appContext: Context? = null
66
+
67
+ @Volatile
68
+ private var lastOptions: Options? = null
69
+
70
+ private data class Options(
71
+ val identity: Identity,
72
+ val theme: ThemeSdk?,
73
+ val parentActivityClassName: String?,
74
+ val notificationIconResourceName: String?,
75
+ val logLevel: SdkLogLevel?,
76
+ val logIncludeSensitive: Boolean?,
77
+ )
78
+
79
+ val apiKey: String?
80
+ get() = synchronized(lock) { (state as? State.Configured)?.identity?.apiKey }
81
+
82
+ /**
83
+ * Whether the SDK finished initialising successfully.
84
+ *
85
+ * Prefers the SDK's own [Carrot.isInit] over our local bookkeeping so a
86
+ * teardown inside the SDK is not reported as still-configured.
87
+ */
88
+ val isConfigured: Boolean
89
+ get() = apiKey != null && runCatching { Carrot.isInit() }
90
+ // Fail closed: if the SDK cannot report its own state, callers must not
91
+ // be told it is ready.
92
+ .onFailure { Log.w(TAG, "Carrot.isInit() threw; reporting not-configured", it) }
93
+ .getOrDefault(false)
94
+
95
+ /**
96
+ * Run an SDK operation once the one application-level setup call settles.
97
+ *
98
+ * Operations arriving while setup is in flight are queued natively. Calls
99
+ * made before setup starts fail immediately, and a failed setup rejects every
100
+ * queued operation with the same error.
101
+ */
102
+ internal fun whenConfigured(completion: (Throwable?) -> Unit) {
103
+ val immediateError = synchronized(lock) {
104
+ when (state) {
105
+ is State.Configured -> null
106
+ is State.Initializing -> {
107
+ readinessWaiters.add(completion)
108
+ return
109
+ }
110
+ State.Idle -> CarrotQuestNotConfiguredException()
111
+ }
112
+ }
113
+
114
+ completion(immediateError)
115
+ }
116
+
117
+ /** Queue a fire-and-forget operation without leaking readiness into JS. */
118
+ internal fun runWhenConfigured(name: String, operation: () -> Unit) {
119
+ whenConfigured ready@ { error ->
120
+ if (error != null) {
121
+ Log.e(TAG, "$name skipped because setup did not complete", error)
122
+ return@ready
123
+ }
124
+
125
+ runCatching(operation)
126
+ .onFailure { Log.e(TAG, "$name failed", it) }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Initialise the SDK.
132
+ *
133
+ * @param context Any context; the application context is retained.
134
+ * @param apiKey Your Carrot quest API key.
135
+ * @param locale Optional locale override, e.g. `"ru"`. The SDK resolves its
136
+ * language from the system locale and exposes no `setLanguage` API, so this
137
+ * calls [Locale.setDefault], which is **process-global** and affects the
138
+ * whole app.
139
+ * @param completion Invoked once, when initialisation settles. Receives
140
+ * [CarrotQuestConfigurationConflictException] if the SDK is already
141
+ * running under a different API key, locale or server.
142
+ */
143
+ @JvmStatic
144
+ @JvmOverloads
145
+ fun configure(
146
+ context: Context,
147
+ apiKey: String,
148
+ locale: String? = null,
149
+ theme: ThemeSdk? = null,
150
+ useEuServer: Boolean? = null,
151
+ parentActivityClassName: String? = null,
152
+ notificationIconResourceName: String? = null,
153
+ logLevel: SdkLogLevel? = null,
154
+ logIncludeSensitive: Boolean? = null,
155
+ completion: ((Throwable?) -> Unit)? = null,
156
+ ) {
157
+ val identity = Identity(apiKey, locale, useEuServer)
158
+
159
+ val options = Options(
160
+ identity = identity,
161
+ theme = theme,
162
+ parentActivityClassName = parentActivityClassName,
163
+ notificationIconResourceName = notificationIconResourceName,
164
+ logLevel = logLevel,
165
+ logIncludeSensitive = logIncludeSensitive,
166
+ )
167
+
168
+ synchronized(lock) {
169
+ when (val current = state) {
170
+ is State.Configured -> {
171
+ val conflict = conflictOrNull(current.identity, identity)
172
+
173
+ if (conflict == null) {
174
+ // Identity matches, so this is a re-configure rather than a second
175
+ // init. Apply the mutable options instead of dropping them — a
176
+ // native launch-time setup often has no theme, and the later JS
177
+ // call is where it arrives.
178
+ applyMutableOptions(context.applicationContext, options)
179
+ lastOptions = mergeMutableOptions(options)
180
+ }
181
+
182
+ completion?.invoke(conflict)
183
+ return
184
+ }
185
+
186
+ is State.Initializing -> {
187
+ val conflict = conflictOrNull(current.identity, identity)
188
+
189
+ if (conflict != null) {
190
+ completion?.invoke(conflict)
191
+ return
192
+ }
193
+
194
+ // Join the in-flight attempt rather than initialising a second time,
195
+ // but carry this caller's options into it so they are not lost.
196
+ lastOptions = mergeMutableOptions(options)
197
+ completion?.let { pending.add(it) }
198
+ return
199
+ }
200
+
201
+ State.Idle -> {
202
+ state = State.Initializing(identity)
203
+ // Publish under the lock. Assigning after releasing it would race a
204
+ // joining caller's merge and clobber their options.
205
+ appContext = context.applicationContext
206
+ lastOptions = options
207
+ completion?.let { pending.add(it) }
208
+ }
209
+ }
210
+ }
211
+
212
+ val applicationContext = context.applicationContext
213
+
214
+ try {
215
+ start(applicationContext, options) { error ->
216
+ if (error == null) {
217
+ settleSuccess(applicationContext, identity, options)
218
+ } else {
219
+ settleFailure(error)
220
+ }
221
+ }
222
+ } catch (error: Throwable) {
223
+ settleFailure(error)
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Fold a later call's mutable options over the recorded ones.
229
+ *
230
+ * Only options the caller actually supplied win, so a second `setup` that
231
+ * omits a field does not clear what an earlier call set. Used to keep
232
+ * [lastOptions] accurate for re-initialisation after logout.
233
+ */
234
+ private fun mergeMutableOptions(options: Options): Options {
235
+ val previous = lastOptions ?: return options
236
+
237
+ return previous.copy(
238
+ identity = options.identity,
239
+ theme = options.theme ?: previous.theme,
240
+ parentActivityClassName =
241
+ options.parentActivityClassName ?: previous.parentActivityClassName,
242
+ notificationIconResourceName =
243
+ options.notificationIconResourceName ?: previous.notificationIconResourceName,
244
+ logLevel = options.logLevel ?: previous.logLevel,
245
+ logIncludeSensitive = options.logIncludeSensitive ?: previous.logIncludeSensitive,
246
+ )
247
+ }
248
+
249
+ /**
250
+ * Apply the options that do not require a fresh initialisation.
251
+ *
252
+ * Safe to call on an already-running SDK; each setter is independent.
253
+ */
254
+ private fun applyMutableOptions(context: Context, options: Options) {
255
+ runCatching {
256
+ options.logLevel?.let { Carrot.setLogLevel(it) }
257
+ options.logIncludeSensitive?.let { Carrot.setLogIncludeSensitive(it) }
258
+ options.theme?.let { Carrot.setTheme(it) }
259
+ options.parentActivityClassName?.let { Carrot.setParentActivityClassName(it) }
260
+ options.notificationIconResourceName?.let { name ->
261
+ resolveDrawable(context, name)?.let { Carrot.setNotificationIcon(it) }
262
+ }
263
+ }.onFailure { Log.w(TAG, "Re-applying setup options failed", it) }
264
+ }
265
+
266
+ /**
267
+ * Refuse to re-point a live SDK at a different account or server.
268
+ *
269
+ * Silently keeping the first configuration would let a second `setup` call
270
+ * resolve successfully while data continued flowing to the wrong Carrot
271
+ * account.
272
+ */
273
+ private fun conflictOrNull(
274
+ existing: Identity,
275
+ requested: Identity,
276
+ ): CarrotQuestConfigurationConflictException? {
277
+ if (existing == requested) return null
278
+
279
+ val differences = buildList {
280
+ if (existing.apiKey != requested.apiKey) add("API key")
281
+ if (existing.locale != requested.locale) add("locale")
282
+ if (existing.useEuServer != requested.useEuServer) add("EU server flag")
283
+ }
284
+
285
+ return CarrotQuestConfigurationConflictException(differences.joinToString(", "))
286
+ }
287
+
288
+ private fun start(
289
+ context: Context,
290
+ options: Options,
291
+ onSettled: (Throwable?) -> Unit,
292
+ ) {
293
+ val identity = options.identity
294
+
295
+ val setupContext = identity.locale
296
+ ?.let { applyLocale(context, it) }
297
+ ?: context
298
+
299
+ // Logging must be configured before setup so initialisation itself is
300
+ // covered by the requested level.
301
+ options.logLevel?.let { Carrot.setLogLevel(it) }
302
+ options.logIncludeSensitive?.let { Carrot.setLogIncludeSensitive(it) }
303
+
304
+ val callback = object : Callback<Boolean> {
305
+ override fun onResponse(result: Boolean?) {
306
+ // The SDK signals success with `true`; anything else is a failed
307
+ // initialisation reported through the success channel.
308
+ if (result != true) {
309
+ onSettled(CarrotQuestSetupRejectedException())
310
+ return
311
+ }
312
+
313
+ // Finalisation owns applying the latest merged options and publishing
314
+ // Configured as one serialised operation. Keeping that work together is
315
+ // what closes the late-join window.
316
+ onSettled(null)
317
+ }
318
+
319
+ override fun onFailure(t: Throwable) {
320
+ Log.e(TAG, "setup failed", t)
321
+ onSettled(t)
322
+ }
323
+ }
324
+
325
+ // SDK 3.x removed the callback-free `setup(context, apiKey)` overload, so
326
+ // there is no path here that reports success before the SDK is ready.
327
+ // The four-argument overload is only used when the caller explicitly opts
328
+ // into EU routing — see CarrotQuestSetupOptions.useEuServer.
329
+ val useEuServer = identity.useEuServer
330
+
331
+ if (useEuServer != null) {
332
+ Carrot.setup(setupContext, identity.apiKey, useEuServer, callback)
333
+ } else {
334
+ Carrot.setup(setupContext, identity.apiKey, callback)
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Apply the final merged options and publish Configured atomically.
340
+ *
341
+ * A matching configure call either acquires [lock] first and contributes its
342
+ * mutable options to [lastOptions], or acquires it after this method and sees
343
+ * [State.Configured], where it applies those options itself. There is no gap
344
+ * in which a caller can join, resolve, and have its options skipped.
345
+ */
346
+ private fun settleSuccess(
347
+ context: Context,
348
+ identity: Identity,
349
+ fallbackOptions: Options,
350
+ ) {
351
+ val waiting: List<(Throwable?) -> Unit>
352
+ val ready: List<(Throwable?) -> Unit>
353
+
354
+ synchronized(lock) {
355
+ applyMutableOptions(context, lastOptions ?: fallbackOptions)
356
+ state = State.Configured(identity)
357
+ waiting = pending.toList()
358
+ pending = mutableListOf()
359
+ ready = readinessWaiters.toList()
360
+ readinessWaiters = mutableListOf()
361
+ }
362
+
363
+ waiting.forEach { it(null) }
364
+ ready.forEach { it(null) }
365
+ }
366
+
367
+ /** Return to Idle after a failed attempt and reject every joined caller. */
368
+ private fun settleFailure(error: Throwable) {
369
+ val waiting: List<(Throwable?) -> Unit>
370
+ val ready: List<(Throwable?) -> Unit>
371
+
372
+ synchronized(lock) {
373
+ state = State.Idle
374
+ waiting = pending.toList()
375
+ pending = mutableListOf()
376
+ ready = readinessWaiters.toList()
377
+ readinessWaiters = mutableListOf()
378
+ }
379
+
380
+ waiting.forEach { it(error) }
381
+ ready.forEach { it(error) }
382
+ }
383
+
384
+ /**
385
+ * Re-initialise after [Carrot.deInit]. The Android SDK tears down its whole
386
+ * client on logout, so it has to be set up again before the next auth.
387
+ *
388
+ * @param completion invoked once re-initialisation settles.
389
+ */
390
+ internal fun reinitialiseAfterLogout(completion: (Throwable?) -> Unit) {
391
+ val context = appContext
392
+ val options = lastOptions
393
+ val identity = synchronized(lock) { (state as? State.Configured)?.identity }
394
+
395
+ if (context == null || options == null || identity == null) {
396
+ completion(null)
397
+ return
398
+ }
399
+
400
+ synchronized(lock) {
401
+ state = State.Initializing(identity)
402
+ }
403
+
404
+ try {
405
+ start(context, options) { error ->
406
+ if (error == null) {
407
+ settleSuccess(context, identity, options)
408
+ } else {
409
+ settleFailure(error)
410
+ }
411
+ completion(error)
412
+ }
413
+ } catch (error: Throwable) {
414
+ settleFailure(error)
415
+ completion(error)
416
+ }
417
+ }
418
+
419
+ private fun resolveDrawable(context: Context, name: String): Int? {
420
+ val id = context.resources.getIdentifier(name, "drawable", context.packageName)
421
+
422
+ if (id == 0) {
423
+ Log.w(TAG, "Notification icon drawable '$name' not found; keeping the SDK default.")
424
+ return null
425
+ }
426
+
427
+ return id
428
+ }
429
+
430
+ /**
431
+ * Override the process locale so the SDK picks up the right language.
432
+ *
433
+ * Must run before `Carrot.setup`. [Locale.setDefault] is process-global, so
434
+ * this affects every locale-sensitive API in the app, not just Carrot.
435
+ */
436
+ private fun applyLocale(context: Context, locale: String): Context {
437
+ val target = Locale(locale)
438
+ Locale.setDefault(target)
439
+
440
+ val config = Configuration(context.resources.configuration)
441
+ config.setLocale(target)
442
+
443
+ return context.createConfigurationContext(config)
444
+ }
445
+ }
446
+
447
+ internal class CarrotQuestNotConfiguredException : IllegalStateException(
448
+ "Carrot quest is not configured. Await setup() first, or call CarrotQuestSetup.configure() from Application.onCreate."
449
+ )
450
+
451
+ internal class CarrotQuestNoActivityException : IllegalStateException(
452
+ "Carrot quest could not open the chat: no foreground activity."
453
+ )
454
+
455
+ internal class CarrotQuestInvalidPayloadException : IllegalArgumentException(
456
+ "Carrot quest could not decode the notification payload as a JSON object."
457
+ )
458
+
459
+ internal class CarrotQuestSetupRejectedException : IllegalStateException(
460
+ "Carrot quest setup did not succeed. Check the API key and network connection."
461
+ )
462
+
463
+ internal class CarrotQuestLogoutRejectedException : IllegalStateException(
464
+ "Carrot quest logout did not succeed; the previous user may still be authenticated."
465
+ )
466
+
467
+ internal class CarrotQuestConfigurationConflictException(differences: String) :
468
+ IllegalStateException(
469
+ "Carrot quest is already initialised with a different $differences. " +
470
+ "The SDK cannot be re-pointed at another account or server at runtime; " +
471
+ "restart the process to change it."
472
+ )