@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/UserGistFeedback.podspec +31 -0
- package/UserGistFeedbackExtension.podspec +23 -0
- package/android/build.gradle +92 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +31 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +153 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +344 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistPushPackage.kt +18 -0
- package/dist/NativeUserGistPush.d.ts +35 -0
- package/dist/NativeUserGistPush.d.ts.map +1 -0
- package/dist/NativeUserGistPush.js +13 -0
- package/dist/NativeUserGistPush.js.map +1 -0
- package/dist/Provider.d.ts +7 -0
- package/dist/Provider.d.ts.map +1 -0
- package/dist/Provider.js +300 -0
- package/dist/Provider.js.map +1 -0
- package/dist/Push.d.ts +103 -0
- package/dist/Push.d.ts.map +1 -0
- package/dist/Push.js +225 -0
- package/dist/Push.js.map +1 -0
- package/dist/UserGist.d.ts +210 -0
- package/dist/UserGist.d.ts.map +1 -0
- package/dist/UserGist.js +1246 -0
- package/dist/UserGist.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/consent.d.ts +17 -0
- package/dist/internal/consent.d.ts.map +1 -0
- package/dist/internal/consent.js +98 -0
- package/dist/internal/consent.js.map +1 -0
- package/dist/internal/context.d.ts +12 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/context.js +70 -0
- package/dist/internal/context.js.map +1 -0
- package/dist/internal/debug.d.ts +14 -0
- package/dist/internal/debug.d.ts.map +1 -0
- package/dist/internal/debug.js +73 -0
- package/dist/internal/debug.js.map +1 -0
- package/dist/internal/engine.d.ts +93 -0
- package/dist/internal/engine.d.ts.map +1 -0
- package/dist/internal/engine.js +904 -0
- package/dist/internal/engine.js.map +1 -0
- package/dist/internal/events.d.ts +49 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +71 -0
- package/dist/internal/events.js.map +1 -0
- package/dist/internal/frequency-cap.d.ts +15 -0
- package/dist/internal/frequency-cap.d.ts.map +1 -0
- package/dist/internal/frequency-cap.js +84 -0
- package/dist/internal/frequency-cap.js.map +1 -0
- package/dist/internal/identity.d.ts +14 -0
- package/dist/internal/identity.d.ts.map +1 -0
- package/dist/internal/identity.js +113 -0
- package/dist/internal/identity.js.map +1 -0
- package/dist/internal/inapp-matcher.d.ts +13 -0
- package/dist/internal/inapp-matcher.d.ts.map +1 -0
- package/dist/internal/inapp-matcher.js +65 -0
- package/dist/internal/inapp-matcher.js.map +1 -0
- package/dist/internal/inapp-rules-cache.d.ts +16 -0
- package/dist/internal/inapp-rules-cache.d.ts.map +1 -0
- package/dist/internal/inapp-rules-cache.js +88 -0
- package/dist/internal/inapp-rules-cache.js.map +1 -0
- package/dist/internal/instruction-dedupe.d.ts +16 -0
- package/dist/internal/instruction-dedupe.d.ts.map +1 -0
- package/dist/internal/instruction-dedupe.js +52 -0
- package/dist/internal/instruction-dedupe.js.map +1 -0
- package/dist/internal/lifecycle.d.ts +12 -0
- package/dist/internal/lifecycle.d.ts.map +1 -0
- package/dist/internal/lifecycle.js +97 -0
- package/dist/internal/lifecycle.js.map +1 -0
- package/dist/internal/modal-coordinator.d.ts +4 -0
- package/dist/internal/modal-coordinator.d.ts.map +1 -0
- package/dist/internal/modal-coordinator.js +43 -0
- package/dist/internal/modal-coordinator.js.map +1 -0
- package/dist/internal/modal-queue.d.ts +9 -0
- package/dist/internal/modal-queue.d.ts.map +1 -0
- package/dist/internal/modal-queue.js +32 -0
- package/dist/internal/modal-queue.js.map +1 -0
- package/dist/internal/mutation-queue.d.ts +23 -0
- package/dist/internal/mutation-queue.d.ts.map +1 -0
- package/dist/internal/mutation-queue.js +66 -0
- package/dist/internal/mutation-queue.js.map +1 -0
- package/dist/internal/prompt-answers.d.ts +6 -0
- package/dist/internal/prompt-answers.d.ts.map +1 -0
- package/dist/internal/prompt-answers.js +10 -0
- package/dist/internal/prompt-answers.js.map +1 -0
- package/dist/internal/prompt-flow.d.ts +8 -0
- package/dist/internal/prompt-flow.d.ts.map +1 -0
- package/dist/internal/prompt-flow.js +26 -0
- package/dist/internal/prompt-flow.js.map +1 -0
- package/dist/internal/queue.d.ts +17 -0
- package/dist/internal/queue.d.ts.map +1 -0
- package/dist/internal/queue.js +172 -0
- package/dist/internal/queue.js.map +1 -0
- package/dist/internal/requests.d.ts +22 -0
- package/dist/internal/requests.d.ts.map +1 -0
- package/dist/internal/requests.js +146 -0
- package/dist/internal/requests.js.map +1 -0
- package/dist/internal/rules-cache.d.ts +16 -0
- package/dist/internal/rules-cache.d.ts.map +1 -0
- package/dist/internal/rules-cache.js +86 -0
- package/dist/internal/rules-cache.js.map +1 -0
- package/dist/internal/storage.d.ts +36 -0
- package/dist/internal/storage.d.ts.map +1 -0
- package/dist/internal/storage.js +213 -0
- package/dist/internal/storage.js.map +1 -0
- package/dist/internal/survey-completion.d.ts +10 -0
- package/dist/internal/survey-completion.d.ts.map +1 -0
- package/dist/internal/survey-completion.js +11 -0
- package/dist/internal/survey-completion.js.map +1 -0
- package/dist/internal/survey-matcher.d.ts +21 -0
- package/dist/internal/survey-matcher.d.ts.map +1 -0
- package/dist/internal/survey-matcher.js +139 -0
- package/dist/internal/survey-matcher.js.map +1 -0
- package/dist/internal/survey-rules-cache.d.ts +17 -0
- package/dist/internal/survey-rules-cache.d.ts.map +1 -0
- package/dist/internal/survey-rules-cache.js +91 -0
- package/dist/internal/survey-rules-cache.js.map +1 -0
- package/dist/internal/survey-store.d.ts +19 -0
- package/dist/internal/survey-store.d.ts.map +1 -0
- package/dist/internal/survey-store.js +59 -0
- package/dist/internal/survey-store.js.map +1 -0
- package/dist/internal/transport.d.ts +210 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +498 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/trigger-matcher.d.ts +21 -0
- package/dist/internal/trigger-matcher.d.ts.map +1 -0
- package/dist/internal/trigger-matcher.js +114 -0
- package/dist/internal/trigger-matcher.js.map +1 -0
- package/dist/internal/types.d.ts +89 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/internal/user-state.d.ts +11 -0
- package/dist/internal/user-state.d.ts.map +1 -0
- package/dist/internal/user-state.js +118 -0
- package/dist/internal/user-state.js.map +1 -0
- package/dist/native/events.d.ts +27 -0
- package/dist/native/events.d.ts.map +1 -0
- package/dist/native/events.js +12 -0
- package/dist/native/events.js.map +1 -0
- package/dist/native/push-bridge.d.ts +20 -0
- package/dist/native/push-bridge.d.ts.map +1 -0
- package/dist/native/push-bridge.js +92 -0
- package/dist/native/push-bridge.js.map +1 -0
- package/dist/preview/PromptShell.d.ts +9 -0
- package/dist/preview/PromptShell.d.ts.map +1 -0
- package/dist/preview/PromptShell.js +128 -0
- package/dist/preview/PromptShell.js.map +1 -0
- package/dist/preview/SurveyShell.d.ts +15 -0
- package/dist/preview/SurveyShell.d.ts.map +1 -0
- package/dist/preview/SurveyShell.js +197 -0
- package/dist/preview/SurveyShell.js.map +1 -0
- package/dist/preview/index.d.ts +4 -0
- package/dist/preview/index.d.ts.map +1 -0
- package/dist/preview/index.js +16 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/ui/InAppMessageView.d.ts +12 -0
- package/dist/ui/InAppMessageView.d.ts.map +1 -0
- package/dist/ui/InAppMessageView.js +260 -0
- package/dist/ui/InAppMessageView.js.map +1 -0
- package/dist/ui/PromptSheet.d.ts +12 -0
- package/dist/ui/PromptSheet.d.ts.map +1 -0
- package/dist/ui/PromptSheet.js +285 -0
- package/dist/ui/PromptSheet.js.map +1 -0
- package/dist/ui/QuestionImageHeader.d.ts +12 -0
- package/dist/ui/QuestionImageHeader.d.ts.map +1 -0
- package/dist/ui/QuestionImageHeader.js +22 -0
- package/dist/ui/QuestionImageHeader.js.map +1 -0
- package/dist/ui/SurveyView.d.ts +25 -0
- package/dist/ui/SurveyView.d.ts.map +1 -0
- package/dist/ui/SurveyView.js +478 -0
- package/dist/ui/SurveyView.js.map +1 -0
- package/dist/ui/animations.d.ts +4 -0
- package/dist/ui/animations.d.ts.map +1 -0
- package/dist/ui/animations.js +24 -0
- package/dist/ui/animations.js.map +1 -0
- package/dist/ui/questions/MultipleChoiceQuestion.d.ts +12 -0
- package/dist/ui/questions/MultipleChoiceQuestion.d.ts.map +1 -0
- package/dist/ui/questions/MultipleChoiceQuestion.js +41 -0
- package/dist/ui/questions/MultipleChoiceQuestion.js.map +1 -0
- package/dist/ui/questions/NpsQuestion.d.ts +14 -0
- package/dist/ui/questions/NpsQuestion.d.ts.map +1 -0
- package/dist/ui/questions/NpsQuestion.js +73 -0
- package/dist/ui/questions/NpsQuestion.js.map +1 -0
- package/dist/ui/questions/RatingQuestion.d.ts +21 -0
- package/dist/ui/questions/RatingQuestion.d.ts.map +1 -0
- package/dist/ui/questions/RatingQuestion.js +91 -0
- package/dist/ui/questions/RatingQuestion.js.map +1 -0
- package/dist/ui/questions/ShortTextQuestion.d.ts +12 -0
- package/dist/ui/questions/ShortTextQuestion.d.ts.map +1 -0
- package/dist/ui/questions/ShortTextQuestion.js +25 -0
- package/dist/ui/questions/ShortTextQuestion.js.map +1 -0
- package/dist/ui/requests/BoardView.d.ts +9 -0
- package/dist/ui/requests/BoardView.d.ts.map +1 -0
- package/dist/ui/requests/BoardView.js +270 -0
- package/dist/ui/requests/BoardView.js.map +1 -0
- package/dist/ui/requests/CommentsSection.d.ts +7 -0
- package/dist/ui/requests/CommentsSection.d.ts.map +1 -0
- package/dist/ui/requests/CommentsSection.js +254 -0
- package/dist/ui/requests/CommentsSection.js.map +1 -0
- package/dist/ui/requests/DetailView.d.ts +8 -0
- package/dist/ui/requests/DetailView.d.ts.map +1 -0
- package/dist/ui/requests/DetailView.js +206 -0
- package/dist/ui/requests/DetailView.js.map +1 -0
- package/dist/ui/requests/RequestsHost.d.ts +12 -0
- package/dist/ui/requests/RequestsHost.d.ts.map +1 -0
- package/dist/ui/requests/RequestsHost.js +58 -0
- package/dist/ui/requests/RequestsHost.js.map +1 -0
- package/dist/ui/requests/StatusPill.d.ts +6 -0
- package/dist/ui/requests/StatusPill.d.ts.map +1 -0
- package/dist/ui/requests/StatusPill.js +25 -0
- package/dist/ui/requests/StatusPill.js.map +1 -0
- package/dist/ui/requests/SubmitView.d.ts +8 -0
- package/dist/ui/requests/SubmitView.d.ts.map +1 -0
- package/dist/ui/requests/SubmitView.js +94 -0
- package/dist/ui/requests/SubmitView.js.map +1 -0
- package/dist/ui/requests/shared.d.ts +21 -0
- package/dist/ui/requests/shared.d.ts.map +1 -0
- package/dist/ui/requests/shared.js +74 -0
- package/dist/ui/requests/shared.js.map +1 -0
- package/dist/ui/survey-questions.d.ts +20 -0
- package/dist/ui/survey-questions.d.ts.map +1 -0
- package/dist/ui/survey-questions.js +211 -0
- package/dist/ui/survey-questions.js.map +1 -0
- package/dist/ui/theme.d.ts +15 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +31 -0
- package/dist/ui/theme.js.map +1 -0
- package/ios/Extension/UserGistNotificationService.swift +292 -0
- package/ios/UserGistPush.h +16 -0
- package/ios/UserGistPush.mm +158 -0
- package/ios/UserGistPushImpl.swift +399 -0
- package/ios/UserGistPushSwizzle.m +168 -0
- package/package.json +89 -0
- package/react-native.config.js +16 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@usergist/feedback-react-native` are documented here.
|
|
4
|
+
Releases use [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## 0.1.0
|
|
7
|
+
|
|
8
|
+
- Initial production React Native SDK.
|
|
9
|
+
- Anonymous and identified-user sessions with consent-aware ingestion.
|
|
10
|
+
- Offline event and mutation queues with bounded retries.
|
|
11
|
+
- Feedback, survey, in-app message, feature-request, and push surfaces.
|
|
12
|
+
- iOS and Android native push bridges plus notification-service extension.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 userGist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @usergist/feedback-react-native
|
|
2
|
+
|
|
3
|
+
Production userGist SDK for React Native and the behavioral reference for the
|
|
4
|
+
iOS, Android, and Flutter packages. Use the dashboard's SDK setup flow to
|
|
5
|
+
verify authentication and ingestion; push releases additionally require APNs
|
|
6
|
+
or FCM configuration and physical-device validation.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @usergist/feedback-react-native \
|
|
12
|
+
@react-native-async-storage/async-storage \
|
|
13
|
+
react-native-safe-area-context
|
|
14
|
+
cd ios && pod install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
AsyncStorage and `react-native-safe-area-context` are peer dependencies.
|
|
18
|
+
Without AsyncStorage the SDK runs with an in-memory store, so queued events do
|
|
19
|
+
not survive a relaunch.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import React from 'react'
|
|
25
|
+
import { AppRegistry } from 'react-native'
|
|
26
|
+
import { UserGist, UserGistProvider } from '@usergist/feedback-react-native'
|
|
27
|
+
|
|
28
|
+
// Obtain this from your authenticated backend. It exchanges an rtk_ token at
|
|
29
|
+
// POST /v1/apps/:appId/sdk/subject-tokens; never ship rtk_ tokens in the app.
|
|
30
|
+
const identityResult = await UserGist.initAsync(
|
|
31
|
+
{
|
|
32
|
+
writeKey: 'rk_live_xxx',
|
|
33
|
+
apiUrl: 'https://api.usergist.com',
|
|
34
|
+
environment: 'production',
|
|
35
|
+
debug: __DEV__,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
userId: 'user_42',
|
|
39
|
+
properties: { plan: 'pro' },
|
|
40
|
+
subjectToken: identifiedSubjectToken,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const consentSynchronized = await UserGist.setConsent({
|
|
45
|
+
analytics: true,
|
|
46
|
+
feedback: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
function App() {
|
|
50
|
+
return (
|
|
51
|
+
<UserGistProvider>
|
|
52
|
+
{/* your app */}
|
|
53
|
+
</UserGistProvider>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
AppRegistry.registerComponent('app', () => App)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
| Method | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `UserGist.init(config)` | Returns synchronously, then hydrates and establishes the anonymous session in the background. |
|
|
65
|
+
| `await UserGist.initAsync(config, initialIdentity?)` | Hydrates and optionally binds a server-proven identity before lifecycle events begin; returns `synced`, `queued`, or `rejected`. |
|
|
66
|
+
| `UserGist.identify(userId, props?, subjectToken)` | Links the anonymous installation using a customer-backend-minted subject token. |
|
|
67
|
+
| `await UserGist.identifyAsync(userId, props?, subjectToken)` | Backward-compatible async identity API returning `synced`, `queued`, or `rejected`. |
|
|
68
|
+
| `UserGist.track(name, props?)` | Enqueues a stable event id and immediately evaluates only server-authorized client-side campaigns; all other decisions remain server-authoritative. |
|
|
69
|
+
| `await UserGist.setConsent({ analytics?, feedback?, push?, survey? })` | Persists and synchronizes the transition, refreshes targeting rules, then resolves with `true`; returns `false` when synchronization fails. |
|
|
70
|
+
| `await UserGist.reset()` | Cancels in-flight, clears queue, rotates anonymous id, wipes caches, and resolves after the new anonymous session is ready. |
|
|
71
|
+
| `UserGist.setThemeOverrides(theme)` | Global theme applied under per-prompt theme. |
|
|
72
|
+
| `UserGist.flush()` | Best-effort flush. |
|
|
73
|
+
| `UserGist.setDebug(boolean)` | Toggle the debug trace logger at runtime. |
|
|
74
|
+
| `UserGist.setDiagnosticHandler(handler)` | Receive bounded diagnostics without raw user payloads. |
|
|
75
|
+
| `UserGist.getAnonymousId()` | Returns the persisted anonymous id (21-char, URL-safe). |
|
|
76
|
+
| `UserGist.getExternalId()` | Returns the current stable external account ID, or `null`. |
|
|
77
|
+
| `UserGist.onPromptShown(cb)` / `UserGist.onResponse(cb)` | Observe prompt lifecycle. |
|
|
78
|
+
|
|
79
|
+
Call and await `reset()` before identifying a different account on the same
|
|
80
|
+
installation. Switching directly from one non-null external ID to another is
|
|
81
|
+
rejected so two people cannot be merged accidentally.
|
|
82
|
+
|
|
83
|
+
### Encrypted persistence
|
|
84
|
+
|
|
85
|
+
By default, the SDK uses AsyncStorage for bounded event/campaign state and
|
|
86
|
+
identity. Signed subject credentials and the durable mutation queue use the
|
|
87
|
+
bundled Keychain/EncryptedSharedPreferences native store and never fall back to
|
|
88
|
+
plaintext; if that native store is unavailable they remain process-memory only.
|
|
89
|
+
Apps that want every persisted value encrypted can provide an encrypted
|
|
90
|
+
key/value adapter **before** init:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
UserGist.setStorageAdapter({
|
|
94
|
+
getItem: (key) => encryptedStore.getItem(key),
|
|
95
|
+
setItem: (key, value) => encryptedStore.setItem(key, value),
|
|
96
|
+
removeItem: (key) => encryptedStore.removeItem(key),
|
|
97
|
+
})
|
|
98
|
+
UserGist.init(config)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The adapter uses the same asynchronous string interface as AsyncStorage, so it
|
|
102
|
+
can wrap the host application's Keychain/Keystore-backed storage.
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
The SDK is a singleton that orchestrates:
|
|
107
|
+
`Storage · Subject session · Consent · Durable queues · Transport · Instruction inbox · Armed campaigns · Persistent user state · Modal UI · Lifecycle · Debug`.
|
|
108
|
+
|
|
109
|
+
AsyncStorage is bounded but not encrypted. Use `setStorageAdapter` when all SDK
|
|
110
|
+
state must be encrypted at rest, and never put credentials or secrets in event
|
|
111
|
+
properties. Default email/phone/SSN/tax-id property keys are removed before
|
|
112
|
+
persistence and again by the API.
|
|
113
|
+
|
|
114
|
+
Public convenience methods contain their own failures and report bounded diagnostics through `setDiagnosticHandler`. Internal UI persistence hooks may reject so the bundled survey UI can show a pending/retry state instead of falsely reporting success.
|
|
115
|
+
|
|
116
|
+
## Push integration modes
|
|
117
|
+
|
|
118
|
+
`UserGist.enablePush()` uses the bundled automatic native integration by default. Apps that already own notification delegates or use another push SDK should use host-forwarded mode:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await UserGist.enablePush({
|
|
122
|
+
environment: 'production',
|
|
123
|
+
installDelegateProxy: false,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Forward the token and message callbacks from the host integration.
|
|
127
|
+
await Push.registerDeviceToken(token, Platform.OS === 'ios' ? 'ios' : 'android')
|
|
128
|
+
Push.handleReceived({ data })
|
|
129
|
+
Push.handleOpened({ data })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
On Android, also disable the bundled `FirebaseMessagingService` when the host owns the FCM service. Set the application manifest placeholder `userGistFirebaseServiceEnabled=false` (or override/remove the library service in the app manifest). Automatic and host-forwarded modes must not both display the same notification.
|
|
133
|
+
|
|
134
|
+
On iOS, `installDelegateProxy: false` prevents UserGist from intercepting `UIApplicationDelegate` and `UNUserNotificationCenter.delegate`. The host remains responsible for forwarding the APNs token and notification callbacks through `Push`.
|
|
135
|
+
|
|
136
|
+
### JSON-driven in-app actions
|
|
137
|
+
|
|
138
|
+
Dashboard-authored in-app CTAs and push action buttons can use the `json`
|
|
139
|
+
action type. The SDK always records the generic button-tap event first, then
|
|
140
|
+
passes the structured object to the host app. Keep business logic in the app
|
|
141
|
+
so the same action can safely select a price, enable a feature, or route into a
|
|
142
|
+
native flow:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
function executeAction(action: Readonly<Record<string, unknown>>) {
|
|
146
|
+
if (action.type === 'enable_feature' && typeof action.feature === 'string') {
|
|
147
|
+
featureStore.enable(action.feature)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
UserGist.setInAppHandlers({
|
|
152
|
+
onJsonAction: (action) => executeAction(action),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
Push.setHandlers({
|
|
156
|
+
onJsonAction: (action) => executeAction(action),
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
JSON actions are data, not executable code. Validate supported `type` values
|
|
161
|
+
and fields in the host before changing app state.
|
|
162
|
+
|
|
163
|
+
## iOS Notification Service Extension (NSE) — required for true delivery tracking
|
|
164
|
+
|
|
165
|
+
The NSE runs in-process when each push arrives, BEFORE the system displays it.
|
|
166
|
+
Without it, on iOS we cannot distinguish "delivered" from "user opened the
|
|
167
|
+
notification" — `delivered_at` only fills when the user actually taps.
|
|
168
|
+
|
|
169
|
+
### One-time Xcode setup
|
|
170
|
+
|
|
171
|
+
1. **File → New → Target → Notification Service Extension**
|
|
172
|
+
- Name: `UserGistNotificationServiceExtension`
|
|
173
|
+
- Bundle id: `<your-app-bundle>.UserGistNotificationServiceExtension`
|
|
174
|
+
2. **Replace the auto-generated `NotificationService.swift`** with:
|
|
175
|
+
```swift
|
|
176
|
+
import UserNotifications
|
|
177
|
+
import UserGistFeedbackExtension
|
|
178
|
+
|
|
179
|
+
class NotificationService: UserGistNotificationService { }
|
|
180
|
+
```
|
|
181
|
+
3. **Configure the NSE target's Info.plist** (or write to the App Group's
|
|
182
|
+
`UserDefaults` from the main app — preferred, supports rotation):
|
|
183
|
+
- `UserGistWriteKey` — required; same write key the main SDK uses.
|
|
184
|
+
- `UserGistApiUrl` — optional; defaults to `https://api.usergist.com`.
|
|
185
|
+
- `UserGistAppGroup` — optional; App Group id shared with main app.
|
|
186
|
+
- `UserGistAnonymousId` — populated by main SDK at init via App Group.
|
|
187
|
+
4. **Add `pod 'UserGistFeedbackExtension'`** to the NSE target stanza in your
|
|
188
|
+
`Podfile` and run `pod install`.
|
|
189
|
+
|
|
190
|
+
The `UserGistNotificationService` base class:
|
|
191
|
+
- Detects silent reachability pings (`usergist_silent: "1"`) and acks them
|
|
192
|
+
— the system shows nothing.
|
|
193
|
+
- Beacons `POST /v1/sdk/push/delivered` with the `usergist.deliveryId` so
|
|
194
|
+
we record true delivered_at, distinct from when the user opens.
|
|
195
|
+
- Falls back to an App Group ledger if the network beacon fails; the main
|
|
196
|
+
app drains it on next foreground.
|
|
197
|
+
- Downloads any rich-media `imageUrl` and attaches it as a
|
|
198
|
+
`UNNotificationAttachment`.
|
|
199
|
+
|
|
200
|
+
## Android NotificationChannels
|
|
201
|
+
|
|
202
|
+
Android 8+ requires every notification to belong to a registered channel —
|
|
203
|
+
notifications without a channel are silently dropped by the OS. The bundled
|
|
204
|
+
Android service creates a `usergist_default` fallback channel automatically.
|
|
205
|
+
When a push names a dashboard-configured `usergist_channel_id`, the service
|
|
206
|
+
uses it if the host app has already created that channel and otherwise falls
|
|
207
|
+
back safely. Host-forwarded integrations can call `await Push.fetchChannels()`
|
|
208
|
+
at startup and create/update those definitions with their notification library.
|
|
209
|
+
|
|
210
|
+
## Forward `applicationDidBecomeActive` / `onResume`
|
|
211
|
+
|
|
212
|
+
Automatic mode sends this beacon from the SDK's foreground listener. In
|
|
213
|
+
host-forwarded mode, call `Push.appDidBecomeActive()` from your foreground hook so the
|
|
214
|
+
server's reachability worker knows this device is alive — skips the next
|
|
215
|
+
silent-ping cycle for this user (saves provider quota + battery).
|
|
216
|
+
|
|
217
|
+
## Compatibility and privacy decisions
|
|
218
|
+
|
|
219
|
+
- Transport bodies use plain JSON so the SDK does not add a native compression dependency.
|
|
220
|
+
- Device model is not collected by default. Send an approved, non-sensitive
|
|
221
|
+
property if your product genuinely needs model-level analysis.
|
|
222
|
+
- Anonymous IDs use a dependency-free, cryptographically random 21-character
|
|
223
|
+
URL-safe generator compatible with nanoid identifiers.
|
|
224
|
+
|
|
225
|
+
## Testing hooks
|
|
226
|
+
|
|
227
|
+
`UserGist.__internal_state()` exposes `{ config, identity, consent, queueSize, triggerCount, frequencyCaps, traces }`. Unstable; do not rely on in production code.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'UserGistFeedback'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.homepage = 'https://github.com/Future-Picnic/usergist-js'
|
|
11
|
+
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
|
12
|
+
s.authors = { 'userGist' => 'engineering@usergist.com' }
|
|
13
|
+
s.platforms = { :ios => '13.0' }
|
|
14
|
+
s.source = { :git => 'https://github.com/Future-Picnic/usergist-js.git', :tag => "v#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.requires_arc = true
|
|
17
|
+
s.swift_version = '5.7'
|
|
18
|
+
|
|
19
|
+
# Default subspec — what RN autolinking picks up for the host app target.
|
|
20
|
+
s.default_subspec = 'Core'
|
|
21
|
+
|
|
22
|
+
# ---------- Core subspec (for the main app target) ----------
|
|
23
|
+
# The RN bridge module: RCTBridgeModule, RCTEventEmitter, Swift impl.
|
|
24
|
+
# Pulls in React-Core because it imports <React/...> headers.
|
|
25
|
+
s.subspec 'Core' do |core|
|
|
26
|
+
core.source_files = 'ios/*.{h,m,mm,swift}'
|
|
27
|
+
core.frameworks = 'UIKit', 'UserNotifications', 'Security'
|
|
28
|
+
core.dependency 'React-Core'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
# The Notification Service Extension is deliberately a separate pod. CocoaPods
|
|
6
|
+
# gives every subspec the root pod's Swift module name, which causes Xcode to
|
|
7
|
+
# infer circular dependencies when the app bridge and extension are embedded in
|
|
8
|
+
# the same workspace. This pod contains no React Native runtime dependency.
|
|
9
|
+
Pod::Spec.new do |s|
|
|
10
|
+
s.name = 'UserGistFeedbackExtension'
|
|
11
|
+
s.version = package['version']
|
|
12
|
+
s.summary = 'UserGist notification service extension support'
|
|
13
|
+
s.description = 'Lightweight iOS notification service extension support for UserGist React Native applications.'
|
|
14
|
+
s.homepage = 'https://github.com/Future-Picnic/usergist-js'
|
|
15
|
+
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
|
16
|
+
s.authors = { 'userGist' => 'engineering@usergist.com' }
|
|
17
|
+
s.platforms = { :ios => '13.0' }
|
|
18
|
+
s.source = { :git => 'https://github.com/Future-Picnic/usergist-js.git', :tag => "v#{s.version}" }
|
|
19
|
+
s.requires_arc = true
|
|
20
|
+
s.swift_version = '5.7'
|
|
21
|
+
s.source_files = 'ios/Extension/*.swift'
|
|
22
|
+
s.frameworks = 'UserNotifications'
|
|
23
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Android build module for @usergist/feedback-react-native.
|
|
2
|
+
//
|
|
3
|
+
// What this module owns:
|
|
4
|
+
// - The native Kotlin React Native module (UserGistPushModule)
|
|
5
|
+
// - A FirebaseMessagingService subclass that auto-registers via the
|
|
6
|
+
// library's AndroidManifest (manifest-merger merges it into the host).
|
|
7
|
+
// - A transitive dep on com.google.firebase:firebase-messaging so the
|
|
8
|
+
// host app does NOT need @react-native-firebase/messaging — they
|
|
9
|
+
// only need their own google-services.json + the gms plugin.
|
|
10
|
+
//
|
|
11
|
+
// Convention: matches what most React Native libraries do (react-native-permissions, etc.).
|
|
12
|
+
|
|
13
|
+
buildscript {
|
|
14
|
+
ext.getExtOrDefault = { name ->
|
|
15
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["UserGistPush_" + name]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
repositories {
|
|
19
|
+
google()
|
|
20
|
+
mavenCentral()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dependencies {
|
|
24
|
+
classpath "com.android.tools.build:gradle:7.2.1"
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
apply plugin: "com.android.library"
|
|
30
|
+
apply plugin: "kotlin-android"
|
|
31
|
+
|
|
32
|
+
def isNewArchitectureEnabled() {
|
|
33
|
+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isNewArchitectureEnabled()) {
|
|
37
|
+
apply plugin: "com.facebook.react"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
android {
|
|
41
|
+
namespace "studio.usergist.feedback"
|
|
42
|
+
compileSdkVersion getExtOrDefault('compileSdkVersion')?.toInteger() ?: 34
|
|
43
|
+
buildToolsVersion getExtOrDefault('buildToolsVersion') ?: '34.0.0'
|
|
44
|
+
|
|
45
|
+
defaultConfig {
|
|
46
|
+
minSdkVersion getExtOrDefault('minSdkVersion')?.toInteger() ?: 23
|
|
47
|
+
targetSdkVersion getExtOrDefault('targetSdkVersion')?.toInteger() ?: 34
|
|
48
|
+
consumerProguardFiles 'consumer-rules.pro'
|
|
49
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
50
|
+
manifestPlaceholders = [userGistFirebaseServiceEnabled: "true"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildFeatures {
|
|
54
|
+
buildConfig true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
compileOptions {
|
|
58
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
59
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
kotlinOptions {
|
|
63
|
+
jvmTarget = "17"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sourceSets {
|
|
67
|
+
main {
|
|
68
|
+
manifest.srcFile "src/main/AndroidManifest.xml"
|
|
69
|
+
java.srcDirs = ['src/main/java']
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
repositories {
|
|
75
|
+
google()
|
|
76
|
+
mavenCentral()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
dependencies {
|
|
80
|
+
implementation "com.facebook.react:react-android"
|
|
81
|
+
|
|
82
|
+
// FCM token + RemoteMessage delivery. Bundled transitively so consumer
|
|
83
|
+
// apps don't add @react-native-firebase/messaging themselves; they only
|
|
84
|
+
// need their own google-services.json + the com.google.gms.google-services
|
|
85
|
+
// plugin in their app/build.gradle (Google's hard requirement for FCM).
|
|
86
|
+
// Use a floor version that's broadly compatible with current Firebase BoM
|
|
87
|
+
// ranges; consumer's BoM (if any) will pick the actual version.
|
|
88
|
+
implementation "com.google.firebase:firebase-messaging:23.4.1"
|
|
89
|
+
|
|
90
|
+
implementation "androidx.core:core-ktx:1.12.0"
|
|
91
|
+
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
|
92
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Preserve the manifest-instantiated FCM service and React Native module in
|
|
2
|
+
# minified host release builds.
|
|
3
|
+
-keep class studio.usergist.feedback.UserGistFirebaseMessagingService { *; }
|
|
4
|
+
-keep class studio.usergist.feedback.UserGistPushModule { *; }
|
|
5
|
+
-keep class studio.usergist.feedback.UserGistPushPackage { *; }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
3
|
+
|
|
4
|
+
<!-- Required for Firebase Cloud Messaging delivery. The host app's
|
|
5
|
+
google-services.json + com.google.gms.google-services plugin
|
|
6
|
+
provide the actual configuration. -->
|
|
7
|
+
|
|
8
|
+
<!-- Android 13+ runtime permission for showing notifications. -->
|
|
9
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
10
|
+
|
|
11
|
+
<application>
|
|
12
|
+
<!-- Library-supplied FirebaseMessagingService. Hosts that already own
|
|
13
|
+
FCM delivery must set userGistFirebaseServiceEnabled=false and
|
|
14
|
+
forward tokens/messages through the public Push API. -->
|
|
15
|
+
<service
|
|
16
|
+
android:name=".UserGistFirebaseMessagingService"
|
|
17
|
+
android:enabled="${userGistFirebaseServiceEnabled}"
|
|
18
|
+
android:exported="false">
|
|
19
|
+
<intent-filter>
|
|
20
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
21
|
+
</intent-filter>
|
|
22
|
+
</service>
|
|
23
|
+
|
|
24
|
+
<!-- Default notification channel applied when the host hasn't
|
|
25
|
+
defined one. Consumers can override by declaring their own
|
|
26
|
+
default-channel meta-data (auto-merged with theirs winning). -->
|
|
27
|
+
<meta-data
|
|
28
|
+
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
29
|
+
android:value="usergist_default" />
|
|
30
|
+
</application>
|
|
31
|
+
</manifest>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package studio.usergist.feedback
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
|
+
import androidx.core.app.NotificationManagerCompat
|
|
11
|
+
import com.facebook.react.bridge.Arguments
|
|
12
|
+
import com.facebook.react.bridge.WritableMap
|
|
13
|
+
import com.google.firebase.messaging.FirebaseMessagingService
|
|
14
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
15
|
+
import org.json.JSONArray
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Library-supplied FirebaseMessagingService. Auto-registered via the
|
|
19
|
+
* library AndroidManifest's manifest-merger entry — consumer apps don't
|
|
20
|
+
* write any Firebase code.
|
|
21
|
+
*
|
|
22
|
+
* - `onNewToken` : forward to JS so the SDK re-registers with the API.
|
|
23
|
+
* - `onMessageReceived` : (a) emit a `notificationReceived` JS event so
|
|
24
|
+
* the host can react, (b) post a system notification
|
|
25
|
+
* when the host app is backgrounded so the user sees
|
|
26
|
+
* the message even when no Activity is alive.
|
|
27
|
+
*
|
|
28
|
+
* If the host app already declares its own FirebaseMessagingService, it must
|
|
29
|
+
* disable this service with the `userGistFirebaseServiceEnabled` manifest
|
|
30
|
+
* placeholder and forward tokens/messages through the public Push API.
|
|
31
|
+
*/
|
|
32
|
+
class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
33
|
+
|
|
34
|
+
override fun onNewToken(token: String) {
|
|
35
|
+
super.onNewToken(token)
|
|
36
|
+
UserGistPushEventBus.emitToken(token)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
40
|
+
super.onMessageReceived(remoteMessage)
|
|
41
|
+
|
|
42
|
+
val payload = normalize(remoteMessage)
|
|
43
|
+
UserGistPushEventBus.emitNotificationReceived(payload)
|
|
44
|
+
|
|
45
|
+
// Surface a system notification when the user-visible content is set.
|
|
46
|
+
// Background / killed-state delivery on Android requires explicit
|
|
47
|
+
// NotificationManager.notify() — RemoteMessage doesn't auto-display.
|
|
48
|
+
val notification = remoteMessage.notification
|
|
49
|
+
val title = notification?.title ?: remoteMessage.data["title"] ?: remoteMessage.data["usergist_title"]
|
|
50
|
+
val body = notification?.body ?: remoteMessage.data["body"] ?: remoteMessage.data["usergist_body"]
|
|
51
|
+
if (title != null || body != null) {
|
|
52
|
+
postSystemNotification(
|
|
53
|
+
title = title,
|
|
54
|
+
body = body,
|
|
55
|
+
data = remoteMessage.data
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun postSystemNotification(title: String?, body: String?, data: Map<String, String>) {
|
|
61
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
62
|
+
checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
63
|
+
) return
|
|
64
|
+
val channelId = resolveChannel(this, data["usergist_channel_id"] ?: data["channel_id"])
|
|
65
|
+
val launchIntent = packageManager?.getLaunchIntentForPackage(packageName)?.apply {
|
|
66
|
+
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
67
|
+
// Forward delivery_id + usergist_delivery_id so when the user taps and
|
|
68
|
+
// the app launches we can fire `$push_opened` with the right id.
|
|
69
|
+
data.forEach { (k, v) ->
|
|
70
|
+
val key = if (k.startsWith("usergist_")) k else "usergist_$k"
|
|
71
|
+
putExtra(key, v)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or
|
|
75
|
+
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
|
76
|
+
val contentIntent = launchIntent?.let {
|
|
77
|
+
PendingIntent.getActivity(this, 0, it, pendingFlags)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val notifId = (System.currentTimeMillis() % Int.MAX_VALUE).toInt()
|
|
81
|
+
val builder = NotificationCompat.Builder(this, channelId)
|
|
82
|
+
.setSmallIcon(applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info)
|
|
83
|
+
.setContentTitle(title)
|
|
84
|
+
.setContentText(body)
|
|
85
|
+
.setAutoCancel(true)
|
|
86
|
+
.setContentIntent(contentIntent)
|
|
87
|
+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
88
|
+
|
|
89
|
+
val rawActions = data["usergist_actions"]
|
|
90
|
+
if (!rawActions.isNullOrBlank() && launchIntent != null) {
|
|
91
|
+
runCatching {
|
|
92
|
+
val actions = JSONArray(rawActions)
|
|
93
|
+
for (index in 0 until actions.length()) {
|
|
94
|
+
val action = actions.optJSONObject(index) ?: continue
|
|
95
|
+
val label = action.optString("label").takeIf { it.isNotBlank() } ?: continue
|
|
96
|
+
val actionIntent = Intent(launchIntent).apply {
|
|
97
|
+
putExtra("usergist_action_identifier", "usergist_action_$index")
|
|
98
|
+
}
|
|
99
|
+
val actionPendingIntent = PendingIntent.getActivity(
|
|
100
|
+
this,
|
|
101
|
+
notifId * 10 + index + 1,
|
|
102
|
+
actionIntent,
|
|
103
|
+
pendingFlags,
|
|
104
|
+
)
|
|
105
|
+
builder.addAction(0, label, actionPendingIntent)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
val nm = NotificationManagerCompat.from(this)
|
|
111
|
+
nm.notify(notifId, builder.build())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun ensureChannel(ctx: Context): String {
|
|
115
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return DEFAULT_CHANNEL_ID
|
|
116
|
+
val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
117
|
+
if (nm.getNotificationChannel(DEFAULT_CHANNEL_ID) == null) {
|
|
118
|
+
val channel = NotificationChannel(
|
|
119
|
+
DEFAULT_CHANNEL_ID,
|
|
120
|
+
"Notifications",
|
|
121
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
122
|
+
)
|
|
123
|
+
nm.createNotificationChannel(channel)
|
|
124
|
+
}
|
|
125
|
+
return DEFAULT_CHANNEL_ID
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun resolveChannel(ctx: Context, requested: String?): String {
|
|
129
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || requested.isNullOrBlank()) {
|
|
130
|
+
return ensureChannel(ctx)
|
|
131
|
+
}
|
|
132
|
+
val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
133
|
+
return if (nm.getNotificationChannel(requested) != null) requested else ensureChannel(ctx)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun normalize(msg: RemoteMessage): WritableMap {
|
|
137
|
+
val out = Arguments.createMap()
|
|
138
|
+
msg.notification?.title?.let { out.putString("title", it) }
|
|
139
|
+
msg.notification?.body?.let { out.putString("body", it) }
|
|
140
|
+
val data = Arguments.createMap()
|
|
141
|
+
for ((k, v) in msg.data) {
|
|
142
|
+
data.putString(k, v)
|
|
143
|
+
}
|
|
144
|
+
out.putMap("data", data)
|
|
145
|
+
val deliveryId = msg.data["delivery_id"] ?: msg.data["usergist_delivery_id"]
|
|
146
|
+
if (deliveryId != null) out.putString("deliveryId", deliveryId)
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
companion object {
|
|
151
|
+
const val DEFAULT_CHANNEL_ID = "usergist_default"
|
|
152
|
+
}
|
|
153
|
+
}
|