@usergist/feedback-react-native 0.1.1 → 0.2.0-beta.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 +8 -0
- package/EXPO-ACCEPTANCE.md +57 -0
- package/EXPO.md +103 -0
- package/README.md +10 -2
- package/UserGistFeedback.podspec +4 -1
- package/UserGistFeedbackExtension.podspec +1 -1
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +78 -12
- package/android/src/main/java/studio/usergist/feedback/UserGistNotificationDismissReceiver.kt +17 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +46 -5
- package/android/src/main/java/studio/usergist/feedback/UserGistPushState.kt +69 -0
- package/app.plugin.js +2 -0
- package/dist/NativeUserGistPush.d.ts +10 -0
- package/dist/NativeUserGistPush.d.ts.map +1 -1
- package/dist/NativeUserGistPush.js.map +1 -1
- package/dist/Push.d.ts.map +1 -1
- package/dist/Push.js +9 -0
- package/dist/Push.js.map +1 -1
- package/dist/UserGist.d.ts +1 -0
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +218 -79
- package/dist/UserGist.js.map +1 -1
- package/dist/expo/index.d.ts +68 -0
- package/dist/expo/index.d.ts.map +1 -0
- package/dist/expo/index.js +120 -0
- package/dist/expo/index.js.map +1 -0
- package/dist/internal/consent.d.ts.map +1 -1
- package/dist/internal/consent.js +4 -1
- package/dist/internal/consent.js.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +6 -1
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/events.d.ts +1 -0
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +6 -0
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/push-dedupe.d.ts +3 -0
- package/dist/internal/push-dedupe.d.ts.map +1 -0
- package/dist/internal/push-dedupe.js +33 -0
- package/dist/internal/push-dedupe.js.map +1 -0
- package/dist/internal/storage.d.ts +1 -0
- package/dist/internal/storage.d.ts.map +1 -1
- package/dist/internal/storage.js +1 -0
- package/dist/internal/storage.js.map +1 -1
- package/dist/internal/transport.d.ts +5 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +2 -2
- package/dist/internal/transport.js.map +1 -1
- package/dist/native/events.d.ts +2 -0
- package/dist/native/events.d.ts.map +1 -1
- package/dist/native/events.js +2 -0
- package/dist/native/events.js.map +1 -1
- package/dist/native/push-adapter.d.ts +11 -0
- package/dist/native/push-adapter.d.ts.map +1 -0
- package/dist/native/push-adapter.js +10 -0
- package/dist/native/push-adapter.js.map +1 -0
- package/dist/native/push-bridge.d.ts +9 -0
- package/dist/native/push-bridge.d.ts.map +1 -1
- package/dist/native/push-bridge.js +39 -4
- package/dist/native/push-bridge.js.map +1 -1
- package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
- package/dist/ui/requests/RequestsHost.js +8 -0
- package/dist/ui/requests/RequestsHost.js.map +1 -1
- package/expo-module.config.json +8 -0
- package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
- package/expo-support/UserGistFeedbackExpo.podspec +17 -0
- package/ios/Extension/UserGistNotificationService.swift +52 -5
- package/ios/Shared/UserGistNotificationActions.swift +27 -0
- package/ios/Shared/UserGistPushState.swift +110 -0
- package/ios/UserGistPush.h +1 -6
- package/ios/UserGistPush.mm +18 -0
- package/ios/UserGistPushImpl.swift +46 -4
- package/ios/UserGistPushInterop.h +5 -0
- package/package.json +33 -7
- package/plugin/index.cjs +269 -0
- package/plugin/index.d.cts +14 -0
- package/plugin/templates/UserGistExpoMessagingService.kt +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0-beta.0
|
|
4
|
+
|
|
5
|
+
- Add Expo config plugin, iOS extension/EAS setup, and an optional notification adapter.
|
|
6
|
+
- Preserve existing Expo notification ownership and shared provider tokens.
|
|
7
|
+
- Add scoped native delivery-receipt retries, Expo onboarding, demo and consumer checks.
|
|
8
|
+
- Keep logout and consent changes from accepting stale request responses or disabling a newer push session.
|
|
9
|
+
- Expo support remains prerelease until signed-device acceptance is complete.
|
|
10
|
+
|
|
3
11
|
All notable changes to `@usergist/feedback-react-native` are documented here.
|
|
4
12
|
Releases use [Semantic Versioning](https://semver.org/).
|
|
5
13
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Expo release evidence
|
|
2
|
+
|
|
3
|
+
Status: implementation under validation; stable release is blocked.
|
|
4
|
+
|
|
5
|
+
Record the exact npm archive SHA256, source commit, Expo/React Native versions,
|
|
6
|
+
build profile, and app identifiers alongside each result. Never record tokens
|
|
7
|
+
or provider private keys. A mocked provider or generated Firebase fixture does
|
|
8
|
+
not count as a live push test.
|
|
9
|
+
|
|
10
|
+
## Automated gates
|
|
11
|
+
|
|
12
|
+
- Packed archive resolves every existing export, `/expo`, the Node config
|
|
13
|
+
plugin, Expo subscriber, Kotlin dispatcher, and notification extension files.
|
|
14
|
+
- Expo 56.0.21 and 57.0.20: automatic and expo-notifications mode, iOS and
|
|
15
|
+
Android, with the New Architecture and repeated non-destructive prebuild.
|
|
16
|
+
- Existing React Native typecheck, unit tests, codegen/package check, and builds.
|
|
17
|
+
- Dashboard/API validation, Expo platform mapping, AI snippets, and onboarding.
|
|
18
|
+
|
|
19
|
+
## Signed-device gates (pending)
|
|
20
|
+
|
|
21
|
+
- Physical iPhone and Android: anonymous startup, identify, logout/account switch,
|
|
22
|
+
offline relaunch and eventual synchronization; no cross-user data or token binding.
|
|
23
|
+
- All reference demo feedback types, surveys, in-app messages, request board,
|
|
24
|
+
comments/votes/follows, targeting, branding, and analytics.
|
|
25
|
+
- UserGist push: foreground/background/terminated launch; token refresh, permission
|
|
26
|
+
denial and later recovery, channels, badge behavior, images, actions, JSON actions,
|
|
27
|
+
deep links, silent reachability and delivered/open evidence.
|
|
28
|
+
- Coexistence: interleave unrelated Expo and UserGist messages, including cold
|
|
29
|
+
starts; one presentation and handler invocation per event/action. Disable
|
|
30
|
+
UserGist and verify unrelated notifications still arrive.
|
|
31
|
+
- Turn off networking while a native receipt is recorded; restore it and verify
|
|
32
|
+
the original delivery timestamp is acknowledged once. Logout before retry and
|
|
33
|
+
verify that old work is never attributed to the new account.
|
|
34
|
+
- iOS development APNs signing and a distribution-signed build, including
|
|
35
|
+
extension embedding, provisioning, App Group persistence and rich delivery.
|
|
36
|
+
|
|
37
|
+
## Promotion
|
|
38
|
+
|
|
39
|
+
Publish a prerelease with npm tag `next` for consumer acceptance after package
|
|
40
|
+
and native build gates pass. Keep stable `latest` unchanged until all signed
|
|
41
|
+
device gates above have evidence. Do not describe Expo support as production
|
|
42
|
+
validated while this document remains pending.
|
|
43
|
+
|
|
44
|
+
The public release workflow enforces this through
|
|
45
|
+
`tools/expo/check-release-evidence.mjs`. Supply the repository variable
|
|
46
|
+
`USERGIST_EXPO_RELEASE_EVIDENCE` as JSON with `version`, `archiveSha256`,
|
|
47
|
+
`sourceCommit`, `nativeBuildRunId`, and arrays `nativeBuilds`, `regressions`, `physicalDevices`.
|
|
48
|
+
Each entry has `name`, `passed: true`, and a durable HTTPS `evidenceUrl` to
|
|
49
|
+
the corresponding build or device report. The script lists the exact required
|
|
50
|
+
names. Prereleases require package, prebuild, ordinary React Native regression,
|
|
51
|
+
and all eight Expo native combinations; stable additionally requires signed
|
|
52
|
+
physical-device evidence. A missing variable blocks publication. CI builds one archive and passes that file
|
|
53
|
+
to every consumer; the publish job downloads it from `nativeBuildRunId` instead
|
|
54
|
+
of rebuilding it. The release tag must point to that run’s source commit.
|
|
55
|
+
|
|
56
|
+
For stable promotion, validate the final stable archive too: changing a
|
|
57
|
+
prerelease version changes the package bytes and invalidates its earlier hash.
|
package/EXPO.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Expo integration (0.2 prerelease)
|
|
2
|
+
|
|
3
|
+
Expo uses the same `UserGist`, `Push`, and `UserGistProvider` as React Native.
|
|
4
|
+
The supported build targets are Expo 56 and 57 on iOS and Android, with the New
|
|
5
|
+
Architecture enabled. Use a development client or production binary. Expo Go
|
|
6
|
+
cannot provide the native bridge or durable secure storage. Browser targets
|
|
7
|
+
use `@usergist/feedback-web` separately.
|
|
8
|
+
|
|
9
|
+
## Base setup
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx expo install @usergist/feedback-react-native \
|
|
13
|
+
@react-native-async-storage/async-storage react-native-safe-area-context
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Add `"@usergist/feedback-react-native"` to `expo.plugins` in app.json or
|
|
17
|
+
app.config.js. Mount `UserGistProvider` once around the existing application:
|
|
18
|
+
in Expo Router's `app/_layout.tsx` wrap `Slot`/`Stack`; otherwise wrap App's
|
|
19
|
+
existing navigation. Call `initAsync` once with the public write key.
|
|
20
|
+
|
|
21
|
+
Anonymous users work without login. For identified users, obtain a scoped
|
|
22
|
+
subject token from your authenticated backend and call `identifyAsync`.
|
|
23
|
+
Never put a server API token in an `EXPO_PUBLIC_` variable. Connect `setConsent`
|
|
24
|
+
to the host's consent decisions, and await `reset()` before changing accounts.
|
|
25
|
+
The consent screen in our demo is a test harness, not a customer requirement.
|
|
26
|
+
|
|
27
|
+
## Push configuration
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
plugins: [
|
|
31
|
+
['@usergist/feedback-react-native', {
|
|
32
|
+
push: {
|
|
33
|
+
mode: 'automatic',
|
|
34
|
+
ios: { apsEnvironment: 'development' },
|
|
35
|
+
// android: { notificationIcon: './assets/notification-icon.png',
|
|
36
|
+
// notificationColor: '#3366FF' },
|
|
37
|
+
},
|
|
38
|
+
}],
|
|
39
|
+
]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set `ios.bundleIdentifier`, `android.package`, and `android.googleServicesFile`.
|
|
43
|
+
The Google JSON is Firebase's client configuration for that package ID, not
|
|
44
|
+
a service-account private key. Upload APNs/FCM sending credentials to UserGist.
|
|
45
|
+
Never include the .p8 or service-account key in an Expo config or application.
|
|
46
|
+
|
|
47
|
+
`apsEnvironment` must match signing: development provisioning uses
|
|
48
|
+
`development`; TestFlight/App Store uses `production`. This is independent of
|
|
49
|
+
the UserGist data environment. Runtime registration reads this plugin setting;
|
|
50
|
+
call `enablePush()` without an environment override to use it.
|
|
51
|
+
|
|
52
|
+
The plugin generates the notification service extension and App Group needed
|
|
53
|
+
for rich media and native iOS delivery receipts. Defaults are
|
|
54
|
+
`UserGistNotificationServiceExtension`, `<bundleId>.UserGistNotificationServiceExtension`,
|
|
55
|
+
and `group.<bundleId>.usergist`. Override these with `extensionTargetName`,
|
|
56
|
+
`extensionBundleIdentifier`, and `appGroupIdentifier` under `push.ios`.
|
|
57
|
+
EAS receives the extension metadata before provisioning. Verify the app and
|
|
58
|
+
extension profiles include the same App Group.
|
|
59
|
+
|
|
60
|
+
Omitting `push` keeps notification acquisition disabled. Initialization and
|
|
61
|
+
configuration never request permission. After the user opts in and consent
|
|
62
|
+
synchronizes, call `enablePush` from a user action. Check `result.granted` and
|
|
63
|
+
`result.error`; `native_module_unavailable` or `push_not_configured` describes
|
|
64
|
+
build configuration, not an OS denial. Restore opt-in only after checking
|
|
65
|
+
current OS permission. `disablePush()` disables UserGist without removing an
|
|
66
|
+
Expo app's shared APNs/FCM registration.
|
|
67
|
+
|
|
68
|
+
## Existing expo-notifications
|
|
69
|
+
|
|
70
|
+
Keep its existing plugin and handlers; place its plugin before UserGist's and
|
|
71
|
+
select `push.mode: 'expo-notifications'`. Configure the adapter before init:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import * as Notifications from 'expo-notifications'
|
|
75
|
+
import { configureExpoNotifications } from '@usergist/feedback-react-native/expo'
|
|
76
|
+
const dispose = configureExpoNotifications(Notifications)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Call once in the app bootstrap, not on every screen mount. Cleanup removes
|
|
80
|
+
the adapter listeners without modifying host notification handlers. The helper
|
|
81
|
+
does not request permissions, grant consent, or register a user on import.
|
|
82
|
+
UserGist uses native device tokens from `getDevicePushTokenAsync`, never
|
|
83
|
+
ExpoPushToken values. The Android dispatcher routes UserGist payloads to our
|
|
84
|
+
native renderer and other messages to Expo. On iOS, Expo retains foreground
|
|
85
|
+
presentation policy and response handling. Your existing Router/deep-link
|
|
86
|
+
handlers remain responsible for navigation.
|
|
87
|
+
|
|
88
|
+
## Rebuilds and conflicts
|
|
89
|
+
|
|
90
|
+
Native/plugin changes require rebuilding with `npx expo run:ios`,
|
|
91
|
+
`npx expo run:android`, or EAS Build. EAS Update cannot add a native module.
|
|
92
|
+
For manually running prebuild against existing directories, Expo 57 requires
|
|
93
|
+
`--no-clean` to preserve those directories; Expo 56 preserves them by default.
|
|
94
|
+
|
|
95
|
+
The plugin does not overwrite a foreign extension target or custom FCM service.
|
|
96
|
+
If one exists, preserve it and integrate the existing host-forwarded `Push`
|
|
97
|
+
API and extension base class following the React Native README. Do not enable
|
|
98
|
+
two FCM services. Automatic composition is supported for expo-notifications;
|
|
99
|
+
other SDKs require an explicit integration. Change ownership modes in a fresh
|
|
100
|
+
native fixture, or review the existing host configuration before migrating.
|
|
101
|
+
|
|
102
|
+
Use the Expo demo and `EXPO-ACCEPTANCE.md` for acceptance. Building or receiving
|
|
103
|
+
a JavaScript callback alone is not physical-device push validation.
|
package/README.md
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Production userGist SDK for React Native and the behavioral reference for the
|
|
4
4
|
iOS, Android, and Flutter packages. Use the dashboard's SDK setup flow to
|
|
5
|
-
verify authentication and ingestion
|
|
6
|
-
|
|
5
|
+
verify authentication and ingestion. The userGist APNs and FCM paths have
|
|
6
|
+
passed end-to-end physical-device validation; each host app still needs its own
|
|
7
|
+
provider credentials, identifiers, native callback setup, and signed-device
|
|
8
|
+
acceptance test.
|
|
9
|
+
|
|
10
|
+
## Expo
|
|
11
|
+
|
|
12
|
+
Expo 56/57 development and production build integration is being validated for
|
|
13
|
+
0.2. Use the same package with its config plugin; see [EXPO.md](EXPO.md).
|
|
14
|
+
Expo Go cannot load the native bridge. See [release gates](EXPO-ACCEPTANCE.md).
|
|
7
15
|
|
|
8
16
|
## Install
|
|
9
17
|
|
package/UserGistFeedback.podspec
CHANGED
|
@@ -23,7 +23,10 @@ Pod::Spec.new do |s|
|
|
|
23
23
|
# The RN bridge module: RCTBridgeModule, RCTEventEmitter, Swift impl.
|
|
24
24
|
# Pulls in React-Core because it imports <React/...> headers.
|
|
25
25
|
s.subspec 'Core' do |core|
|
|
26
|
-
core.source_files = 'ios/*.{h,m,mm,swift}'
|
|
26
|
+
core.source_files = 'ios/*.{h,m,mm,swift}', 'ios/Shared/*.swift'
|
|
27
|
+
# This shim is internal to the Obj-C implementation. Exporting its React
|
|
28
|
+
# imports through our Swift umbrella can hide React types in host modules.
|
|
29
|
+
core.private_header_files = 'ios/UserGistPush.h'
|
|
27
30
|
core.frameworks = 'UIKit', 'UserNotifications', 'Security'
|
|
28
31
|
core.dependency 'React-Core'
|
|
29
32
|
end
|
|
@@ -18,6 +18,6 @@ Pod::Spec.new do |s|
|
|
|
18
18
|
s.source = { :git => 'https://github.com/Future-Picnic/usergist-js.git', :tag => "v#{s.version}" }
|
|
19
19
|
s.requires_arc = true
|
|
20
20
|
s.swift_version = '5.7'
|
|
21
|
-
s.source_files = 'ios/Extension/*.swift'
|
|
21
|
+
s.source_files = 'ios/Extension/*.swift', 'ios/Shared/*.swift'
|
|
22
22
|
s.frameworks = 'UserNotifications'
|
|
23
23
|
end
|
package/android/build.gradle
CHANGED
|
@@ -85,7 +85,9 @@ dependencies {
|
|
|
85
85
|
// plugin in their app/build.gradle (Google's hard requirement for FCM).
|
|
86
86
|
// Use a floor version that's broadly compatible with current Firebase BoM
|
|
87
87
|
// ranges; consumer's BoM (if any) will pick the actual version.
|
|
88
|
-
|
|
88
|
+
// The public native dispatcher accepts RemoteMessage, so host adapters need
|
|
89
|
+
// Firebase on their compile classpath as well as at runtime.
|
|
90
|
+
api "com.google.firebase:firebase-messaging:23.4.1"
|
|
89
91
|
|
|
90
92
|
implementation "androidx.core:core-ktx:1.12.0"
|
|
91
93
|
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
10
10
|
|
|
11
11
|
<application>
|
|
12
|
+
<receiver android:name="studio.usergist.feedback.UserGistNotificationDismissReceiver" android:exported="false" />
|
|
12
13
|
<!-- Library-supplied FirebaseMessagingService. Hosts that already own
|
|
13
14
|
FCM delivery must set userGistFirebaseServiceEnabled=false and
|
|
14
15
|
forward tokens/messages through the public Push API. -->
|
|
@@ -31,15 +31,30 @@ import org.json.JSONArray
|
|
|
31
31
|
*/
|
|
32
32
|
class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
33
33
|
|
|
34
|
-
override fun onNewToken(token: String)
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
override fun onNewToken(token: String) = handleToken(token)
|
|
35
|
+
override fun onMessageReceived(message: RemoteMessage) = handleMessage(this, message)
|
|
36
|
+
companion object {
|
|
37
|
+
@JvmStatic fun handleToken(token: String) = UserGistPushEventBus.emitToken(token)
|
|
38
|
+
@JvmStatic fun handleMessage(context: Context, message: RemoteMessage) {
|
|
39
|
+
if (!UserGistPushState.enabled(context)) return
|
|
40
|
+
// FCM already invokes this on its worker thread. Keep the service alive
|
|
41
|
+
// until presentation completes instead of leaving detached work behind.
|
|
42
|
+
UserGistMessageHandler(context.applicationContext).receive(message)
|
|
43
|
+
}
|
|
37
44
|
}
|
|
45
|
+
}
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
private class UserGistMessageHandler(context: Context) : android.content.ContextWrapper(context) {
|
|
48
|
+
fun receive(remoteMessage: RemoteMessage) {
|
|
49
|
+
val recipient = remoteMessage.data["usergist_anonymous_id"] ?: UserGistPushState.anonymousId(this)
|
|
50
|
+
if (!UserGistPushState.accepts(this, recipient)) return
|
|
51
|
+
if (remoteMessage.data["usergist_silent"] == "1") {
|
|
52
|
+
UserGistPushState.receipt(this, "silent-ack", remoteMessage.data["usergist_ping_id"] ?: "")
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (!remoteMessage.data.containsKey("usergist_campaign_id")) return
|
|
56
|
+
UserGistPushState.receipt(this, "delivered", remoteMessage.data["usergist_delivery_id"] ?: "")
|
|
57
|
+
val payload = normalize(remoteMessage, recipient)
|
|
43
58
|
UserGistPushEventBus.emitNotificationReceived(payload)
|
|
44
59
|
|
|
45
60
|
// Surface a system notification when the user-visible content is set.
|
|
@@ -52,7 +67,7 @@ class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
|
52
67
|
postSystemNotification(
|
|
53
68
|
title = title,
|
|
54
69
|
body = body,
|
|
55
|
-
data = remoteMessage.data
|
|
70
|
+
data = remoteMessage.data + ("usergist_anonymous_id" to recipient)
|
|
56
71
|
)
|
|
57
72
|
}
|
|
58
73
|
}
|
|
@@ -74,17 +89,22 @@ class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
|
74
89
|
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or
|
|
75
90
|
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
|
76
91
|
val contentIntent = launchIntent?.let {
|
|
77
|
-
PendingIntent.getActivity(this,
|
|
92
|
+
PendingIntent.getActivity(this, (data["usergist_delivery_id"] ?: data.toString()).hashCode(), it, pendingFlags)
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
val notifId = (
|
|
95
|
+
val notifId = (data["usergist_delivery_id"] ?: data.toString()).hashCode()
|
|
81
96
|
val builder = NotificationCompat.Builder(this, channelId)
|
|
82
|
-
.setSmallIcon(
|
|
97
|
+
.setSmallIcon(packageManager.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_META_DATA).metaData?.getInt("UserGistNotificationIcon", 0)?.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info)
|
|
83
98
|
.setContentTitle(title)
|
|
84
99
|
.setContentText(body)
|
|
85
100
|
.setAutoCancel(true)
|
|
86
101
|
.setContentIntent(contentIntent)
|
|
87
102
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
103
|
+
data["usergist_badge"]?.let { raw ->
|
|
104
|
+
val badges = getSharedPreferences("usergist_push_badges", Context.MODE_PRIVATE)
|
|
105
|
+
val count = if (raw == "+1") badges.getInt("count", 0) + 1 else raw.toIntOrNull()
|
|
106
|
+
if (count != null && count >= 0) { builder.setNumber(count); badges.edit().putInt("count", count).apply() }
|
|
107
|
+
}
|
|
88
108
|
|
|
89
109
|
val rawActions = data["usergist_actions"]
|
|
90
110
|
if (!rawActions.isNullOrBlank() && launchIntent != null) {
|
|
@@ -107,8 +127,53 @@ class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
|
|
130
|
+
val color = packageManager.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_META_DATA).metaData?.get("UserGistNotificationColor")
|
|
131
|
+
when (color) {
|
|
132
|
+
is Int -> builder.setColor(color)
|
|
133
|
+
is String -> runCatching { builder.setColor(android.graphics.Color.parseColor(color)) }
|
|
134
|
+
}
|
|
135
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && data["usergist_sound"] != "none") {
|
|
136
|
+
val sound = data["usergist_sound"]
|
|
137
|
+
val custom = sound?.takeIf { it != "default" }?.let { resources.getIdentifier(it.substringBeforeLast('.'), "raw", packageName) } ?: 0
|
|
138
|
+
builder.setSound(if (custom != 0) android.net.Uri.parse("android.resource://$packageName/$custom") else android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION))
|
|
139
|
+
}
|
|
140
|
+
data["usergist_image_url"]?.let { raw ->
|
|
141
|
+
var connection: java.net.HttpURLConnection? = null
|
|
142
|
+
try {
|
|
143
|
+
val url = java.net.URL(raw)
|
|
144
|
+
if (url.protocol == "https") {
|
|
145
|
+
connection = url.openConnection() as java.net.HttpURLConnection
|
|
146
|
+
connection.connectTimeout = 4000; connection.readTimeout = 4000
|
|
147
|
+
connection.instanceFollowRedirects = false
|
|
148
|
+
if (connection.responseCode in 200..299) {
|
|
149
|
+
val bytes = connection.inputStream.use { stream ->
|
|
150
|
+
val out = java.io.ByteArrayOutputStream(); val buffer = ByteArray(8192)
|
|
151
|
+
while (out.size() <= 5 * 1024 * 1024) { val count = stream.read(buffer); if (count < 0) break; out.write(buffer, 0, count) }
|
|
152
|
+
out.toByteArray()
|
|
153
|
+
}
|
|
154
|
+
if (bytes.size <= 5 * 1024 * 1024) {
|
|
155
|
+
val bounds = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
156
|
+
android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
|
157
|
+
val decode = android.graphics.BitmapFactory.Options().apply { inSampleSize = (maxOf(bounds.outWidth, bounds.outHeight) / 1024).coerceAtLeast(1) }
|
|
158
|
+
android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size, decode)?.let { builder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(it)) }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (_: Exception) { /* Text notification still displays. */ }
|
|
163
|
+
finally { connection?.disconnect() }
|
|
164
|
+
}
|
|
165
|
+
val dismiss = Intent(this, UserGistNotificationDismissReceiver::class.java).apply {
|
|
166
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
167
|
+
}
|
|
168
|
+
builder.setDeleteIntent(PendingIntent.getBroadcast(this, notifId, dismiss, pendingFlags))
|
|
110
169
|
val nm = NotificationManagerCompat.from(this)
|
|
170
|
+
if (!UserGistPushState.accepts(this, data["usergist_anonymous_id"]) || !nm.areNotificationsEnabled()) return
|
|
171
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(channelId)?.importance == NotificationManager.IMPORTANCE_NONE) return
|
|
111
172
|
nm.notify(notifId, builder.build())
|
|
173
|
+
UserGistPushState.receipt(this, "displayed", data["usergist_delivery_id"] ?: "")
|
|
174
|
+
val displayed = Arguments.createMap()
|
|
175
|
+
displayed.putMap("data", Arguments.createMap().apply { data.forEach { (k, v) -> putString(k, v) } })
|
|
176
|
+
UserGistPushEventBus.emitNotificationDisplayed(displayed)
|
|
112
177
|
}
|
|
113
178
|
|
|
114
179
|
private fun ensureChannel(ctx: Context): String {
|
|
@@ -133,7 +198,7 @@ class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
|
133
198
|
return if (nm.getNotificationChannel(requested) != null) requested else ensureChannel(ctx)
|
|
134
199
|
}
|
|
135
200
|
|
|
136
|
-
private fun normalize(msg: RemoteMessage): WritableMap {
|
|
201
|
+
private fun normalize(msg: RemoteMessage, recipient: String): WritableMap {
|
|
137
202
|
val out = Arguments.createMap()
|
|
138
203
|
msg.notification?.title?.let { out.putString("title", it) }
|
|
139
204
|
msg.notification?.body?.let { out.putString("body", it) }
|
|
@@ -141,6 +206,7 @@ class UserGistFirebaseMessagingService : FirebaseMessagingService() {
|
|
|
141
206
|
for ((k, v) in msg.data) {
|
|
142
207
|
data.putString(k, v)
|
|
143
208
|
}
|
|
209
|
+
data.putString("usergist_anonymous_id", recipient)
|
|
144
210
|
out.putMap("data", data)
|
|
145
211
|
val deliveryId = msg.data["delivery_id"] ?: msg.data["usergist_delivery_id"]
|
|
146
212
|
if (deliveryId != null) out.putString("deliveryId", deliveryId)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package studio.usergist.feedback
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
|
|
7
|
+
class UserGistNotificationDismissReceiver : BroadcastReceiver() {
|
|
8
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
9
|
+
if (!UserGistPushState.accepts(context, intent.getStringExtra("usergist_anonymous_id"))) return
|
|
10
|
+
UserGistPushState.receipt(context, "dismissed", intent.getStringExtra("usergist_delivery_id") ?: "")
|
|
11
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
12
|
+
body.putMap("data", com.facebook.react.bridge.Arguments.createMap().apply {
|
|
13
|
+
intent.extras?.keySet()?.forEach { key -> intent.getStringExtra(key)?.let { putString(key, it) } }
|
|
14
|
+
})
|
|
15
|
+
UserGistPushEventBus.emitNotificationDismissed(body)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -49,6 +49,12 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
override fun getName(): String = NAME
|
|
52
|
+
override fun invalidate() {
|
|
53
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
54
|
+
reactApplicationContext.removeActivityEventListener(this)
|
|
55
|
+
UserGistPushEventBus.detach(reactApplicationContext)
|
|
56
|
+
super.invalidate()
|
|
57
|
+
}
|
|
52
58
|
|
|
53
59
|
private val securePreferences by lazy {
|
|
54
60
|
val masterKey = MasterKey.Builder(reactApplicationContext)
|
|
@@ -64,7 +70,8 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
override fun onHostResume() {
|
|
67
|
-
|
|
73
|
+
UserGistPushState.retry(reactApplicationContext)
|
|
74
|
+
reactApplicationContext.currentActivity?.intent?.let { consumeNotificationIntent(it) }
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
override fun onHostPause() = Unit
|
|
@@ -77,6 +84,7 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
77
84
|
|
|
78
85
|
private fun consumeNotificationIntent(intent: Intent) {
|
|
79
86
|
val extras = intent.extras ?: return
|
|
87
|
+
if (!UserGistPushState.accepts(reactApplicationContext, extras.getString("usergist_anonymous_id"))) return
|
|
80
88
|
val keys = extras.keySet().filter { it.startsWith("usergist_") }
|
|
81
89
|
if (keys.isEmpty()) return
|
|
82
90
|
val body = Arguments.createMap()
|
|
@@ -99,8 +107,23 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
99
107
|
|
|
100
108
|
// MARK: - Exported methods
|
|
101
109
|
|
|
110
|
+
@ReactMethod
|
|
111
|
+
fun getPushConfiguration(promise: Promise) {
|
|
112
|
+
promise.resolve(Arguments.createMap().apply { putString("mode", UserGistPushState.mode(reactApplicationContext)) })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@ReactMethod
|
|
116
|
+
fun configurePushState(state: ReadableMap, promise: Promise) {
|
|
117
|
+
runCatching { UserGistPushState.configure(reactApplicationContext, org.json.JSONObject(state.toHashMap())) }
|
|
118
|
+
.onSuccess { promise.resolve(null) }.onFailure { promise.reject("push_state_failed", it) }
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
@ReactMethod
|
|
103
122
|
fun enablePush(_options: ReadableMap?, promise: Promise) {
|
|
123
|
+
if (UserGistPushState.mode(reactApplicationContext) == "disabled") {
|
|
124
|
+
promise.resolve(buildResult(false, "not_determined", null, "push_not_configured"))
|
|
125
|
+
return
|
|
126
|
+
}
|
|
104
127
|
// Android < 13: notification permission is install-time, no runtime
|
|
105
128
|
// prompt. We can short-circuit straight to the token fetch.
|
|
106
129
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
@@ -112,7 +135,7 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
112
135
|
fetchTokenAndResolve(promise, status = "authorized")
|
|
113
136
|
return
|
|
114
137
|
}
|
|
115
|
-
val activity = currentActivity
|
|
138
|
+
val activity = reactApplicationContext.currentActivity
|
|
116
139
|
if (activity == null) {
|
|
117
140
|
// No activity: can't ask for permission. Resolve with current state.
|
|
118
141
|
val granted = ActivityCompat.checkSelfPermission(
|
|
@@ -165,6 +188,8 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
165
188
|
|
|
166
189
|
@ReactMethod
|
|
167
190
|
fun disablePush(promise: Promise) {
|
|
191
|
+
UserGistPushState.disable(reactApplicationContext)
|
|
192
|
+
if (UserGistPushState.isExpo(reactApplicationContext)) { promise.resolve(null); return }
|
|
168
193
|
FirebaseMessaging.getInstance().deleteToken()
|
|
169
194
|
.addOnCompleteListener { promise.resolve(null) }
|
|
170
195
|
}
|
|
@@ -233,10 +258,10 @@ class UserGistPushModule(reactContext: ReactApplicationContext) :
|
|
|
233
258
|
// Required for RN's NativeEventEmitter API even though we emit from a
|
|
234
259
|
// separate static bus — RN will warn otherwise.
|
|
235
260
|
@ReactMethod
|
|
236
|
-
fun addListener(_eventName: String?) {
|
|
261
|
+
fun addListener(_eventName: String?) { UserGistPushEventBus.addListener() }
|
|
237
262
|
|
|
238
263
|
@ReactMethod
|
|
239
|
-
fun removeListeners(_count: Double) {
|
|
264
|
+
fun removeListeners(_count: Double) { UserGistPushEventBus.removeListeners(_count.toInt()) }
|
|
240
265
|
|
|
241
266
|
// MARK: - Internal
|
|
242
267
|
|
|
@@ -287,10 +312,12 @@ internal object UserGistPushEventBus {
|
|
|
287
312
|
@Volatile private var reactContext: ReactApplicationContext? = null
|
|
288
313
|
private val pending: MutableList<Pair<String, WritableMap>> = mutableListOf()
|
|
289
314
|
private var initialNotification: WritableMap? = null
|
|
315
|
+
private var listenerCount = 0
|
|
290
316
|
|
|
291
317
|
@Synchronized
|
|
292
318
|
fun attach(ctx: ReactApplicationContext) {
|
|
293
319
|
this.reactContext = ctx
|
|
320
|
+
if (listenerCount == 0) return
|
|
294
321
|
val drained = pending.toList()
|
|
295
322
|
pending.clear()
|
|
296
323
|
for ((name, body) in drained) {
|
|
@@ -298,6 +325,20 @@ internal object UserGistPushEventBus {
|
|
|
298
325
|
}
|
|
299
326
|
}
|
|
300
327
|
|
|
328
|
+
@Synchronized
|
|
329
|
+
fun addListener() {
|
|
330
|
+
listenerCount++
|
|
331
|
+
// Drain after all JS subscriptions in this tick have been registered.
|
|
332
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
333
|
+
synchronized(this) { if (listenerCount > 0) reactContext?.let { attach(it) } }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
@Synchronized fun removeListeners(count: Int) { listenerCount = (listenerCount - count).coerceAtLeast(0) }
|
|
337
|
+
@Synchronized fun detach(ctx: ReactApplicationContext) { if (reactContext === ctx) { reactContext = null; listenerCount = 0 } }
|
|
338
|
+
|
|
339
|
+
fun emitNotificationDisplayed(body: WritableMap) = emit("UserGistPush:notificationDisplayed", body)
|
|
340
|
+
fun emitNotificationDismissed(body: WritableMap) = emit("UserGistPush:notificationDismissed", body)
|
|
341
|
+
|
|
301
342
|
fun emitToken(token: String) {
|
|
302
343
|
val map = Arguments.createMap()
|
|
303
344
|
map.putString("token", token)
|
|
@@ -328,7 +369,7 @@ internal object UserGistPushEventBus {
|
|
|
328
369
|
@Synchronized
|
|
329
370
|
private fun emit(name: String, body: WritableMap) {
|
|
330
371
|
val ctx = reactContext
|
|
331
|
-
if (ctx == null || !ctx.hasActiveReactInstance()) {
|
|
372
|
+
if (ctx == null || !ctx.hasActiveReactInstance() || listenerCount == 0) {
|
|
332
373
|
if (pending.size >= 100) pending.removeAt(0)
|
|
333
374
|
pending.add(name to body)
|
|
334
375
|
return
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package studio.usergist.feedback
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.net.HttpURLConnection
|
|
7
|
+
import java.net.URL
|
|
8
|
+
import java.text.SimpleDateFormat
|
|
9
|
+
import java.util.Date
|
|
10
|
+
import java.util.Locale
|
|
11
|
+
import java.util.TimeZone
|
|
12
|
+
import java.util.UUID
|
|
13
|
+
import java.util.concurrent.Executors
|
|
14
|
+
|
|
15
|
+
internal object UserGistPushState {
|
|
16
|
+
private val worker = Executors.newSingleThreadExecutor()
|
|
17
|
+
private fun prefs(context: Context) = context.getSharedPreferences("usergist_push_state", Context.MODE_PRIVATE)
|
|
18
|
+
fun isExpo(context: Context): Boolean = metadata(context).getBoolean("UserGistExpo", false)
|
|
19
|
+
fun mode(context: Context): String = metadata(context).getString("UserGistPushMode") ?: "automatic"
|
|
20
|
+
private fun metadata(context: Context) = context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData ?: android.os.Bundle()
|
|
21
|
+
fun enabled(context: Context): Boolean = !isExpo(context) || prefs(context).getBoolean("push", false)
|
|
22
|
+
fun anonymousId(context: Context): String = JSONObject(prefs(context).getString("state", "{}")!!).optString("anonymousId")
|
|
23
|
+
fun accepts(context: Context, scope: String?): Boolean = enabled(context) && (scope.isNullOrEmpty() || scope == anonymousId(context))
|
|
24
|
+
private fun receiptPrefix(state: JSONObject) = "receipt." + UUID.nameUUIDFromBytes((state.optString("writeKey") + ":" + state.optString("anonymousId")).toByteArray(Charsets.UTF_8)) + "."
|
|
25
|
+
@Synchronized fun configure(context: Context, state: JSONObject) {
|
|
26
|
+
check(prefs(context).edit().putString("state", state.toString()).putBoolean("push", state.optBoolean("push")).commit())
|
|
27
|
+
if (state.optBoolean("push")) retry(context)
|
|
28
|
+
}
|
|
29
|
+
fun disable(context: Context) { prefs(context).edit().putBoolean("push", false).commit() }
|
|
30
|
+
fun receipt(context: Context, kind: String, id: String) {
|
|
31
|
+
if (id.isEmpty() || !enabled(context)) return
|
|
32
|
+
val state = JSONObject(prefs(context).getString("state", "{}")!!)
|
|
33
|
+
if (state.optString("writeKey").isEmpty()) return
|
|
34
|
+
val now = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date())
|
|
35
|
+
val body = if (kind == "silent-ack") JSONObject().put("pingId", id).put("anonymousId", state.optString("anonymousId")).put("receivedAt", now)
|
|
36
|
+
else JSONObject().put("deliveryId", id).put("occurredAt", now)
|
|
37
|
+
val store = prefs(context)
|
|
38
|
+
val prefix = receiptPrefix(state)
|
|
39
|
+
val record = JSONObject().put("state", state).put("kind", kind).put("body", body)
|
|
40
|
+
val key = prefix + UUID.randomUUID()
|
|
41
|
+
if (store.edit().putString(key, record.toString()).commit()) worker.execute { send(context, key, record) }
|
|
42
|
+
}
|
|
43
|
+
fun retry(context: Context) {
|
|
44
|
+
worker.execute {
|
|
45
|
+
val prefix = receiptPrefix(JSONObject(prefs(context).getString("state", "{}")!!))
|
|
46
|
+
prefs(context).all.filterKeys { it.startsWith(prefix) }.entries.take(500).forEach { (key, value) ->
|
|
47
|
+
runCatching { send(context, key, JSONObject(value as String)) }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
private fun send(context: Context, key: String, record: JSONObject) {
|
|
52
|
+
val store = prefs(context)
|
|
53
|
+
val current = JSONObject(store.getString("state", "{}")!!)
|
|
54
|
+
val state = record.getJSONObject("state")
|
|
55
|
+
if (!store.getBoolean("push", false) || current.optString("anonymousId") != state.optString("anonymousId") || current.optString("writeKey") != state.optString("writeKey")) return
|
|
56
|
+
var connection: HttpURLConnection? = null
|
|
57
|
+
try {
|
|
58
|
+
connection = URL(state.getString("apiUrl").trimEnd('/') + "/v1/sdk/push/" + record.getString("kind")).openConnection() as HttpURLConnection
|
|
59
|
+
connection.connectTimeout = 5000; connection.readTimeout = 5000
|
|
60
|
+
connection.instanceFollowRedirects = false
|
|
61
|
+
connection.requestMethod = "POST"; connection.doOutput = true
|
|
62
|
+
connection.setRequestProperty("Authorization", "Bearer " + state.getString("writeKey"))
|
|
63
|
+
connection.setRequestProperty("Content-Type", "application/json")
|
|
64
|
+
connection.outputStream.use { it.write(record.getJSONObject("body").toString().toByteArray(Charsets.UTF_8)) }
|
|
65
|
+
if (connection.responseCode in 200..299) store.edit().remove(key).commit()
|
|
66
|
+
} catch (_: Exception) { /* Retry after foregrounding; retain the original receipt scope. */ }
|
|
67
|
+
finally { connection?.disconnect() }
|
|
68
|
+
}
|
|
69
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -14,6 +14,16 @@ export interface EnablePushResult {
|
|
|
14
14
|
}
|
|
15
15
|
export type PushPermissionStatus = 'authorized' | 'provisional' | 'denied' | 'not_determined';
|
|
16
16
|
export interface Spec extends TurboModule {
|
|
17
|
+
getPushConfiguration?(): Promise<{
|
|
18
|
+
mode?: string;
|
|
19
|
+
environment?: 'sandbox' | 'production';
|
|
20
|
+
}>;
|
|
21
|
+
configurePushState?(state: {
|
|
22
|
+
writeKey: string;
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
anonymousId: string;
|
|
25
|
+
push: boolean;
|
|
26
|
+
}): Promise<void>;
|
|
17
27
|
enablePush(options: {
|
|
18
28
|
[key: string]: unknown;
|
|
19
29
|
} | null): Promise<EnablePushResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeUserGistPush.d.ts","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAOA,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"}
|
|
1
|
+
{"version":3,"file":"NativeUserGistPush.d.ts","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAOA,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,oBAAoB,CAAC,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAA;KAAE,CAAC,CAAA;IAC3F,kBAAkB,CAAC,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnH,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeUserGistPush.js","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,4EAA4E;AAC5E,yDAAyD;AAGzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"NativeUserGistPush.js","sourceRoot":"","sources":["../src/NativeUserGistPush.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,4EAA4E;AAC5E,yDAAyD;AAGzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAwClD,eAAe,mBAAmB,CAAC,GAAG,CAAO,cAAc,CAAC,CAAA"}
|