@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPushAdapter } from './push-adapter.js';
|
|
1
2
|
// Runtime bridge to the UserGist native push module.
|
|
2
3
|
//
|
|
3
4
|
// Strategy:
|
|
@@ -19,14 +20,26 @@ function warnMissing() {
|
|
|
19
20
|
return;
|
|
20
21
|
warned = true;
|
|
21
22
|
// eslint-disable-next-line no-console
|
|
22
|
-
console.warn('[usergist] Native push module not found. Make sure @usergist/feedback-react-native is installed and rebuild the app (`pod install` for iOS, rebuild for Android).
|
|
23
|
+
console.warn('[usergist] Native push module not found. Make sure @usergist/feedback-react-native is installed and rebuild the app (`pod install` for iOS, rebuild for Android). Expo Go cannot load this module. Use an Expo development build (npx expo run:ios / run:android) or EAS Build; an OTA update cannot add native code.');
|
|
23
24
|
}
|
|
24
25
|
const STUB_RESULT = {
|
|
25
26
|
granted: false,
|
|
26
|
-
status: '
|
|
27
|
+
status: 'not_determined',
|
|
28
|
+
error: 'native_module_unavailable',
|
|
27
29
|
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
28
30
|
};
|
|
29
31
|
export const UserGistPushNative = {
|
|
32
|
+
async defaultEnvironment() {
|
|
33
|
+
return (await Module?.getPushConfiguration?.())?.environment;
|
|
34
|
+
},
|
|
35
|
+
async syncState(state) {
|
|
36
|
+
if (!Module) {
|
|
37
|
+
warnMissing();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const configuration = await Module?.getPushConfiguration?.();
|
|
41
|
+
await Module?.configurePushState?.({ ...state, push: state.push && configuration?.mode !== 'disabled' });
|
|
42
|
+
},
|
|
30
43
|
isAvailable() {
|
|
31
44
|
return Module != null;
|
|
32
45
|
},
|
|
@@ -35,6 +48,14 @@ export const UserGistPushNative = {
|
|
|
35
48
|
warnMissing();
|
|
36
49
|
return STUB_RESULT;
|
|
37
50
|
}
|
|
51
|
+
const configuration = await Module.getPushConfiguration?.();
|
|
52
|
+
if (configuration?.mode === 'disabled')
|
|
53
|
+
return { ...STUB_RESULT, error: 'push_not_configured' };
|
|
54
|
+
const adapter = getPushAdapter();
|
|
55
|
+
if (configuration?.mode === 'expo-notifications' && !adapter)
|
|
56
|
+
return { ...STUB_RESULT, error: 'expo_adapter_required' };
|
|
57
|
+
if (adapter)
|
|
58
|
+
return adapter.enablePush(options);
|
|
38
59
|
return Module.enablePush(options);
|
|
39
60
|
},
|
|
40
61
|
async disablePush() {
|
|
@@ -42,16 +63,28 @@ export const UserGistPushNative = {
|
|
|
42
63
|
warnMissing();
|
|
43
64
|
return;
|
|
44
65
|
}
|
|
66
|
+
const adapter = getPushAdapter();
|
|
67
|
+
if (adapter)
|
|
68
|
+
await adapter.disablePush();
|
|
69
|
+
// Native Expo mode disables only UserGist, never the host APNs/FCM token.
|
|
45
70
|
await Module.disablePush();
|
|
46
71
|
},
|
|
47
72
|
async getPermissionStatus() {
|
|
48
|
-
if (!Module)
|
|
49
|
-
|
|
73
|
+
if (!Module) {
|
|
74
|
+
warnMissing();
|
|
75
|
+
return 'not_determined';
|
|
76
|
+
}
|
|
77
|
+
const adapter = getPushAdapter();
|
|
78
|
+
if (adapter)
|
|
79
|
+
return adapter.getPermissionStatus();
|
|
50
80
|
return Module.getPushPermissionStatus();
|
|
51
81
|
},
|
|
52
82
|
async setBadgeCount(count) {
|
|
53
83
|
if (!Module)
|
|
54
84
|
return;
|
|
85
|
+
const adapter = getPushAdapter();
|
|
86
|
+
if (adapter)
|
|
87
|
+
await adapter.setBadgeCount(count);
|
|
55
88
|
await Module.setBadgeCount(count);
|
|
56
89
|
},
|
|
57
90
|
async getInitialNotification() {
|
|
@@ -89,4 +122,6 @@ export const onTokenReceived = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.TOKEN_RE
|
|
|
89
122
|
export const onTokenError = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.TOKEN_ERROR, cb);
|
|
90
123
|
export const onNotificationReceived = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_RECEIVED, cb);
|
|
91
124
|
export const onNotificationOpened = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_OPENED, cb);
|
|
125
|
+
export const onNotificationDisplayed = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_DISPLAYED, cb);
|
|
126
|
+
export const onNotificationDismissed = (cb) => onPushEvent(USERGIST_PUSH_EVENTS.NOTIFICATION_DISMISSED, cb);
|
|
92
127
|
//# sourceMappingURL=push-bridge.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push-bridge.js","sourceRoot":"","sources":["../../src/native/push-bridge.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,EAAE;AACF,YAAY;AACZ,+EAA+E;AAC/E,wDAAwD;AACxD,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,oDAAoD;AAEpD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE1E,OAAO,WAAW,MAAM,0BAA0B,CAAA;AAClD,OAAO,EACL,oBAAoB,GAKrB,MAAM,aAAa,CAAA;AAEpB,8DAA8D;AAC9D,MAAM,YAAY,GAAsB,aAAqB,CAAC,YAAY,CAAA;AAE1E,MAAM,MAAM,GAAgB,WAAW,IAAI,YAAY,IAAI,IAAI,CAAA;AAE/D,IAAI,MAAM,GAAG,KAAK,CAAA;AAClB,SAAS,WAAW;IAClB,IAAI,MAAM;QAAE,OAAM;IAClB,MAAM,GAAG,IAAI,CAAA;IACb,sCAAsC;IACtC,OAAO,CAAC,IAAI,CACV,
|
|
1
|
+
{"version":3,"file":"push-bridge.js","sourceRoot":"","sources":["../../src/native/push-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestsHost.d.ts","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAelD,wBAAgB,YAAY,IAAI,KAAK,CAAC,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"RequestsHost.d.ts","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAelD,wBAAgB,YAAY,IAAI,KAAK,CAAC,YAAY,GAAG,IAAI,CAyExD"}
|
|
@@ -22,6 +22,8 @@ export function RequestsHost() {
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
let unsubBoard = null;
|
|
24
24
|
let unsubDetail = null;
|
|
25
|
+
let unsubReset = null;
|
|
26
|
+
let unsubDismiss = null;
|
|
25
27
|
let retryTimer = null;
|
|
26
28
|
function attach() {
|
|
27
29
|
try {
|
|
@@ -32,6 +34,8 @@ export function RequestsHost() {
|
|
|
32
34
|
unsubDetail = bus.on('showRequestDetail', ({ requestId }) => {
|
|
33
35
|
setView({ kind: 'detail', requestId });
|
|
34
36
|
});
|
|
37
|
+
unsubReset = bus.on('resetSurfaces', () => setView({ kind: 'closed' }));
|
|
38
|
+
unsubDismiss = bus.on('dismissRequests', () => setView({ kind: 'closed' }));
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
41
|
retryTimer = setTimeout(attach, 250);
|
|
@@ -45,6 +49,10 @@ export function RequestsHost() {
|
|
|
45
49
|
unsubBoard();
|
|
46
50
|
if (unsubDetail)
|
|
47
51
|
unsubDetail();
|
|
52
|
+
if (unsubReset)
|
|
53
|
+
unsubReset();
|
|
54
|
+
if (unsubDismiss)
|
|
55
|
+
unsubDismiss();
|
|
48
56
|
};
|
|
49
57
|
}, []);
|
|
50
58
|
const visible = view.kind !== 'closed';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestsHost.js","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;AACH,OAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAClD,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAQlE,MAAM,UAAU,YAAY;IAC1B,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC/D,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAE9B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,GAAwB,IAAI,CAAA;QAC1C,IAAI,WAAW,GAAwB,IAAI,CAAA;QAC3C,IAAI,UAAU,GAAyC,IAAI,CAAA;QAE3D,SAAS,MAAM;YACb,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,QAAQ,CAAC,iBAAiB,EAAE,CAAA;gBACxC,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;oBAC5C,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;gBAC5B,CAAC,CAAC,CAAA;gBACF,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE;oBAC1D,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;gBACxC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"RequestsHost.js","sourceRoot":"","sources":["../../../src/ui/requests/RequestsHost.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UserGistFeedback
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
public class UserGistExpoAppDelegateSubscriber: ExpoAppDelegateSubscriber {
|
|
6
|
+
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
7
|
+
UserGistPushImpl.shared.prepareExpoLaunch()
|
|
8
|
+
return true
|
|
9
|
+
}
|
|
10
|
+
public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
|
|
11
|
+
UserGistPushImpl.shared.recordToken(deviceToken: token)
|
|
12
|
+
}
|
|
13
|
+
public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
14
|
+
UserGistPushImpl.shared.recordTokenError(error: error)
|
|
15
|
+
}
|
|
16
|
+
public func applicationDidBecomeActive(_ application: UIApplication) {
|
|
17
|
+
_ = UserGistPushState.updateBadge(count: application.applicationIconBadgeNumber)
|
|
18
|
+
UserGistPushState.retry()
|
|
19
|
+
}
|
|
20
|
+
public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
|
21
|
+
if userInfo["usergist_silent"] as? String == "1", let pingId = userInfo["usergist_ping_id"] as? String {
|
|
22
|
+
if let recipient = userInfo["usergist_anonymous_id"] as? String,
|
|
23
|
+
recipient != UserGistPushState.snapshot["anonymousId"] as? String {
|
|
24
|
+
completionHandler(.noData)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
UserGistPushState.receipt("silent-ack", id: pingId) { success in
|
|
28
|
+
completionHandler(success ? .newData : .failed)
|
|
29
|
+
}
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
completionHandler(.noData)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, '../package.json')))
|
|
3
|
+
Pod::Spec.new do |s|
|
|
4
|
+
s.name = 'UserGistFeedbackExpo'
|
|
5
|
+
s.version = package['version']
|
|
6
|
+
s.summary = 'Expo lifecycle integration for UserGist'
|
|
7
|
+
s.homepage = package['homepage']
|
|
8
|
+
s.license = { :type => 'MIT', :file => '../LICENSE' }
|
|
9
|
+
s.authors = { 'UserGist' => 'engineering@usergist.com' }
|
|
10
|
+
s.source = { :git => 'https://github.com/Future-Picnic/usergist-js.git' }
|
|
11
|
+
s.platforms = { :ios => '15.1' }
|
|
12
|
+
s.swift_version = '5.0'
|
|
13
|
+
s.static_framework = true
|
|
14
|
+
s.source_files = '*.swift'
|
|
15
|
+
s.dependency 'ExpoModulesCore'
|
|
16
|
+
s.dependency 'UserGistFeedback', package['version']
|
|
17
|
+
end
|
|
@@ -57,6 +57,21 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
57
57
|
self.bestAttempt = request.content.mutableCopy() as? UNMutableNotificationContent
|
|
58
58
|
|
|
59
59
|
let userInfo = request.content.userInfo
|
|
60
|
+
let pushState = UserGistPushState.snapshot
|
|
61
|
+
if userInfo["usergist"] == nil && userInfo["usergist_silent"] as? String != "1" {
|
|
62
|
+
finish(request.content)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled {
|
|
66
|
+
finish(userInfo["usergist"] == nil ? request.content : UNNotificationContent())
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
|
|
70
|
+
let recipient = userInfo["usergist_anonymous_id"] as? String,
|
|
71
|
+
recipient != pushState["anonymousId"] as? String {
|
|
72
|
+
finish(UNNotificationContent())
|
|
73
|
+
return
|
|
74
|
+
}
|
|
60
75
|
|
|
61
76
|
// ---------- Silent reachability ping ----------
|
|
62
77
|
if (userInfo["usergist_silent"] as? String) == "1" {
|
|
@@ -72,11 +87,25 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
72
87
|
finish(request.content)
|
|
73
88
|
return
|
|
74
89
|
}
|
|
90
|
+
if bestAttempt.userInfo["usergist_anonymous_id"] == nil, let anonymousId = pushState["anonymousId"] as? String {
|
|
91
|
+
bestAttempt.userInfo["usergist_anonymous_id"] = anonymousId
|
|
92
|
+
}
|
|
93
|
+
if userInfo["usergist_badge_increment"] as? Bool == true {
|
|
94
|
+
bestAttempt.badge = UserGistPushState.updateBadge(increment: true)
|
|
95
|
+
} else if let badge = bestAttempt.badge {
|
|
96
|
+
_ = UserGistPushState.updateBadge(count: badge.intValue)
|
|
97
|
+
}
|
|
98
|
+
let actionsReady = DispatchGroup()
|
|
99
|
+
actionsReady.enter()
|
|
100
|
+
UserGistNotificationActions.configure(bestAttempt) { actionsReady.leave() }
|
|
75
101
|
|
|
76
102
|
// ---------- Delivered beacon (direct, with App Group fallback) ----------
|
|
77
103
|
if let deliveryId = extractDeliveryId(from: userInfo) {
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
actionsReady.enter()
|
|
105
|
+
UserGistBeaconClient.delivered(deliveryId: deliveryId) { actionsReady.leave() }
|
|
106
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool != true {
|
|
107
|
+
UserGistDeliveryLedger.recordDelivered(deliveryId: deliveryId)
|
|
108
|
+
}
|
|
80
109
|
}
|
|
81
110
|
|
|
82
111
|
if let subtitle = userInfo["subtitle"] as? String {
|
|
@@ -87,7 +116,7 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
87
116
|
let imageUrlString = extractImageUrl(from: userInfo)
|
|
88
117
|
|
|
89
118
|
guard let raw = imageUrlString, let imageUrl = URL(string: raw) else {
|
|
90
|
-
finish(bestAttempt)
|
|
119
|
+
actionsReady.notify(queue: .main) { self.finish(bestAttempt) }
|
|
91
120
|
return
|
|
92
121
|
}
|
|
93
122
|
|
|
@@ -95,7 +124,7 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
95
124
|
if let attachment = attachment {
|
|
96
125
|
bestAttempt.attachments = [attachment]
|
|
97
126
|
}
|
|
98
|
-
self.finish(bestAttempt)
|
|
127
|
+
actionsReady.notify(queue: .main) { self.finish(bestAttempt) }
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
130
|
|
|
@@ -116,6 +145,15 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
116
145
|
let handler = contentHandler
|
|
117
146
|
contentHandler = nil
|
|
118
147
|
completionLock.unlock()
|
|
148
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
|
|
149
|
+
content.userInfo["usergist"] != nil {
|
|
150
|
+
let state = UserGistPushState.snapshot
|
|
151
|
+
if state["push"] as? Bool != true ||
|
|
152
|
+
(content.userInfo["usergist_anonymous_id"] as? String).map({ $0 != state["anonymousId"] as? String }) == true {
|
|
153
|
+
handler?(UNNotificationContent())
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
}
|
|
119
157
|
handler?(content)
|
|
120
158
|
}
|
|
121
159
|
|
|
@@ -173,7 +211,12 @@ open class UserGistNotificationService: UNNotificationServiceExtension {
|
|
|
173
211
|
/// + URLSession's background-friendly default config.
|
|
174
212
|
enum UserGistBeaconClient {
|
|
175
213
|
|
|
176
|
-
static func delivered(deliveryId: String) {
|
|
214
|
+
static func delivered(deliveryId: String, completion: @escaping () -> Void = {}) {
|
|
215
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true {
|
|
216
|
+
UserGistPushState.receipt("delivered", id: deliveryId) { _ in completion() }
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
defer { completion() }
|
|
177
220
|
guard !deliveryId.isEmpty else { return }
|
|
178
221
|
guard let writeKey = UserGistNSEConfig.writeKey,
|
|
179
222
|
let apiUrl = UserGistNSEConfig.apiUrl else { return }
|
|
@@ -192,6 +235,10 @@ enum UserGistBeaconClient {
|
|
|
192
235
|
}
|
|
193
236
|
|
|
194
237
|
static func silentAck(pingId: String, completion: @escaping () -> Void) {
|
|
238
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true {
|
|
239
|
+
UserGistPushState.receipt("silent-ack", id: pingId) { _ in completion() }
|
|
240
|
+
return
|
|
241
|
+
}
|
|
195
242
|
guard !pingId.isEmpty else { return completion() }
|
|
196
243
|
guard let writeKey = UserGistNSEConfig.writeKey,
|
|
197
244
|
let apiUrl = UserGistNSEConfig.apiUrl,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import UserNotifications
|
|
4
|
+
|
|
5
|
+
enum UserGistNotificationActions {
|
|
6
|
+
static func configure(_ content: UNMutableNotificationContent, completion: @escaping () -> Void) {
|
|
7
|
+
guard let payload = content.userInfo["usergist"] as? [String: Any] else { completion(); return }
|
|
8
|
+
let buttons = payload["actionButtons"] as? [[String: Any]] ?? []
|
|
9
|
+
let labels = buttons.prefix(4).enumerated().compactMap { index, button -> (Int, String)? in
|
|
10
|
+
guard let label = button["label"] as? String, !label.isEmpty else { return nil }
|
|
11
|
+
return (index, label)
|
|
12
|
+
}
|
|
13
|
+
let encoded = (try? JSONSerialization.data(withJSONObject: labels.map { [$0.0, $0.1] as [Any] })) ?? Data()
|
|
14
|
+
let suffix = SHA256.hash(data: encoded).map { String(format: "%02x", $0) }.joined()
|
|
15
|
+
let identifier = "USERGIST_" + suffix
|
|
16
|
+
let actions = labels.map { UNNotificationAction(identifier: "usergist_action_\($0.0)", title: $0.1, options: [.foreground]) }
|
|
17
|
+
let category = UNNotificationCategory(identifier: identifier, actions: actions, intentIdentifiers: [], options: [.customDismissAction])
|
|
18
|
+
let center = UNUserNotificationCenter.current()
|
|
19
|
+
center.getNotificationCategories { categories in
|
|
20
|
+
var merged = categories
|
|
21
|
+
merged.insert(category)
|
|
22
|
+
center.setNotificationCategories(merged)
|
|
23
|
+
content.categoryIdentifier = identifier
|
|
24
|
+
completion()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
/// Shared by the app and NSE. Each receipt has its own atomic file, avoiding
|
|
6
|
+
/// read/modify/write races between the two processes. No subject credentials.
|
|
7
|
+
public enum UserGistPushState {
|
|
8
|
+
private static var group: String? { Bundle.main.object(forInfoDictionaryKey: "UserGistAppGroup") as? String }
|
|
9
|
+
private static var root: URL? {
|
|
10
|
+
guard let group = group else { return nil }
|
|
11
|
+
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)?.appendingPathComponent("usergist", isDirectory: true)
|
|
12
|
+
}
|
|
13
|
+
private static var stateURL: URL? { root?.appendingPathComponent("state.json") }
|
|
14
|
+
public static var snapshot: [String: Any] {
|
|
15
|
+
guard let url = stateURL, let data = try? Data(contentsOf: url),
|
|
16
|
+
let state = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
|
17
|
+
return state
|
|
18
|
+
}
|
|
19
|
+
public static var enabled: Bool { snapshot["push"] as? Bool == true }
|
|
20
|
+
public static func updateBadge(count: Int? = nil, increment: Bool = false) -> NSNumber? {
|
|
21
|
+
guard let root = root else { return count.map { NSNumber(value: max(0, $0)) } }
|
|
22
|
+
do { try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) } catch { return nil }
|
|
23
|
+
let descriptor = Darwin.open(root.appendingPathComponent("badge.lock").path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR)
|
|
24
|
+
guard descriptor >= 0 else { return nil }
|
|
25
|
+
defer { Darwin.close(descriptor) }
|
|
26
|
+
guard flock(descriptor, LOCK_EX) == 0 else { return nil }
|
|
27
|
+
defer { flock(descriptor, LOCK_UN) }
|
|
28
|
+
let file = root.appendingPathComponent("badge.json")
|
|
29
|
+
let previous = (try? String(contentsOf: file, encoding: .utf8)).flatMap(Int.init) ?? 0
|
|
30
|
+
let next = max(0, count ?? (increment ? min(previous, Int.max - 1) + 1 : previous))
|
|
31
|
+
do { try Data(String(next).utf8).write(to: file, options: .atomic); return NSNumber(value: next) }
|
|
32
|
+
catch { return nil }
|
|
33
|
+
}
|
|
34
|
+
private static func receiptsFolder(_ state: [String: Any]) -> URL? {
|
|
35
|
+
let scope = "\(state["writeKey"] as? String ?? ""):\(state["anonymousId"] as? String ?? "")"
|
|
36
|
+
let hash = SHA256.hash(data: Data(scope.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
37
|
+
return root?.appendingPathComponent("receipts", isDirectory: true).appendingPathComponent(hash, isDirectory: true)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static func configure(_ state: [String: Any]) throws {
|
|
41
|
+
guard let root = root, let url = stateURL else { return }
|
|
42
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
43
|
+
try JSONSerialization.data(withJSONObject: state).write(to: url, options: .atomic)
|
|
44
|
+
if state["push"] as? Bool == true { retry() }
|
|
45
|
+
}
|
|
46
|
+
public static func disable() {
|
|
47
|
+
var state = snapshot
|
|
48
|
+
state["push"] = false
|
|
49
|
+
try? configure(state)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static func receipt(_ kind: String, id: String, completion: @escaping (Bool) -> Void = { _ in }) {
|
|
53
|
+
let state = snapshot
|
|
54
|
+
guard state["push"] as? Bool == true, !id.isEmpty, let folder = receiptsFolder(state),
|
|
55
|
+
let key = state["writeKey"] as? String,
|
|
56
|
+
let api = state["apiUrl"] as? String,
|
|
57
|
+
let anonymousId = state["anonymousId"] as? String else { completion(false); return }
|
|
58
|
+
do {
|
|
59
|
+
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
|
|
60
|
+
let now = ISO8601DateFormatter().string(from: Date())
|
|
61
|
+
let body: [String: Any] = kind == "silent-ack"
|
|
62
|
+
? ["pingId": id, "anonymousId": anonymousId, "receivedAt": now]
|
|
63
|
+
: ["deliveryId": id, "occurredAt": now]
|
|
64
|
+
let record: [String: Any] = ["kind": kind, "body": body, "writeKey": key, "apiUrl": api, "anonymousId": anonymousId]
|
|
65
|
+
let file = folder.appendingPathComponent(UUID().uuidString + ".json")
|
|
66
|
+
try JSONSerialization.data(withJSONObject: record).write(to: file, options: .atomic)
|
|
67
|
+
send(file, completion: completion)
|
|
68
|
+
} catch { completion(false) }
|
|
69
|
+
}
|
|
70
|
+
public static func retry() {
|
|
71
|
+
guard let folder = receiptsFolder(snapshot),
|
|
72
|
+
let files = try? FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) else { return }
|
|
73
|
+
files.prefix(500).forEach { send($0) }
|
|
74
|
+
}
|
|
75
|
+
private static func send(_ file: URL, completion: @escaping (Bool) -> Void = { _ in }) {
|
|
76
|
+
guard let data = try? Data(contentsOf: file),
|
|
77
|
+
let record = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
78
|
+
let key = record["writeKey"] as? String, let api = record["apiUrl"] as? String,
|
|
79
|
+
let kind = record["kind"] as? String, let body = record["body"] as? [String: Any],
|
|
80
|
+
let base = URL(string: api), ["https", "http"].contains(base.scheme ?? "") else { completion(false); return }
|
|
81
|
+
let state = snapshot
|
|
82
|
+
// Never replay old identity receipts with a new session's configuration.
|
|
83
|
+
guard state["push"] as? Bool == true, state["writeKey"] as? String == key,
|
|
84
|
+
state["anonymousId"] as? String == record["anonymousId"] as? String else { completion(false); return }
|
|
85
|
+
var request = URLRequest(url: base.appendingPathComponent("v1/sdk/push/" + kind), timeoutInterval: 5)
|
|
86
|
+
request.httpMethod = "POST"
|
|
87
|
+
request.setValue("Bearer " + key, forHTTPHeaderField: "Authorization")
|
|
88
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
89
|
+
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
|
90
|
+
let lock = NSLock()
|
|
91
|
+
var completed = false
|
|
92
|
+
let finish: (Bool) -> Void = { success in
|
|
93
|
+
lock.lock()
|
|
94
|
+
guard !completed else { lock.unlock(); return }
|
|
95
|
+
completed = true
|
|
96
|
+
lock.unlock()
|
|
97
|
+
completion(success)
|
|
98
|
+
}
|
|
99
|
+
URLSession.shared.dataTask(with: request) { _, response, error in
|
|
100
|
+
let success = error == nil && (200..<300).contains((response as? HTTPURLResponse)?.statusCode ?? 0)
|
|
101
|
+
if success {
|
|
102
|
+
try? FileManager.default.removeItem(at: file)
|
|
103
|
+
}
|
|
104
|
+
finish(success)
|
|
105
|
+
}.resume()
|
|
106
|
+
// Keep the extension/background task alive for the acknowledgement attempt,
|
|
107
|
+
// while retaining a bounded completion even if the connection stalls.
|
|
108
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 5.5) { finish(false) }
|
|
109
|
+
}
|
|
110
|
+
}
|
package/ios/UserGistPush.h
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <React/RCTEventEmitter.h>
|
|
3
3
|
#import <Foundation/Foundation.h>
|
|
4
|
+
#import "UserGistPushInterop.h"
|
|
4
5
|
|
|
5
6
|
// Public Objective-C bridge. The actual implementation lives in
|
|
6
7
|
// UserGistPushImpl.swift; this class is a thin shim that React Native's
|
|
@@ -8,9 +9,3 @@
|
|
|
8
9
|
// or a TurboModule registration).
|
|
9
10
|
@interface UserGistPush : RCTEventEmitter <RCTBridgeModule>
|
|
10
11
|
@end
|
|
11
|
-
|
|
12
|
-
// Exposed by UserGistPushSwizzle.m. Returns whoever last tried to install
|
|
13
|
-
// themselves as the UNUserNotificationCenter delegate (FirebaseAnalytics,
|
|
14
|
-
// OneSignal, etc.). The Swift delegate uses this to chain didReceive /
|
|
15
|
-
// willPresent so other SDKs' analytics still fire while we keep ownership.
|
|
16
|
-
NSObject * _Nullable _UserGistGetLastForeignDelegate(void);
|
package/ios/UserGistPush.mm
CHANGED
|
@@ -44,6 +44,8 @@ RCT_EXPORT_MODULE(UserGistPush)
|
|
|
44
44
|
@"UserGistPush:tokenError",
|
|
45
45
|
@"UserGistPush:notificationReceived",
|
|
46
46
|
@"UserGistPush:notificationOpened",
|
|
47
|
+
@"UserGistPush:notificationDisplayed",
|
|
48
|
+
@"UserGistPush:notificationDismissed",
|
|
47
49
|
];
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -56,6 +58,22 @@ RCT_EXPORT_MODULE(UserGistPush)
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// MARK: - Exported methods
|
|
61
|
+
RCT_EXPORT_METHOD(getPushConfiguration:(RCTPromiseResolveBlock)resolve
|
|
62
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
63
|
+
NSString *mode = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UserGistPushMode"];
|
|
64
|
+
NSString *environment = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UserGistApsEnvironment"];
|
|
65
|
+
NSMutableDictionary *result = [NSMutableDictionary new];
|
|
66
|
+
if (mode) result[@"mode"] = mode;
|
|
67
|
+
if (environment) result[@"environment"] = [environment isEqualToString:@"production"] ? @"production" : @"sandbox";
|
|
68
|
+
resolve(result);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
RCT_EXPORT_METHOD(configurePushState:(NSDictionary *)state
|
|
72
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
73
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
74
|
+
[UserGistPushImpl.shared configurePushState:state resolver:resolve rejecter:reject];
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
|
|
60
78
|
RCT_EXPORT_METHOD(enablePush:(NSDictionary *)options
|
|
61
79
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
@@ -69,16 +69,37 @@ public final class UserGistPushImpl: NSObject {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
@objc public func configurePushState(_ state: [String: Any], resolver: @escaping (Any?) -> Void, rejecter: @escaping (String, String, Error?) -> Void) {
|
|
73
|
+
do { try UserGistPushState.configure(state); resolver(nil) }
|
|
74
|
+
catch { rejecter("push_state_failed", error.localizedDescription, error) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Restore notification callbacks before iOS delivers a cold-start response.
|
|
78
|
+
/// This only restores an existing opt-in; it does not acquire permission or
|
|
79
|
+
/// register a token. Expo Notifications retains ownership in coexistence mode.
|
|
80
|
+
public func prepareExpoLaunch() {
|
|
81
|
+
guard Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true,
|
|
82
|
+
Bundle.main.object(forInfoDictionaryKey: "UserGistPushMode") as? String == "automatic",
|
|
83
|
+
UserGistPushState.enabled else { return }
|
|
84
|
+
Self.installUNDelegateSwizzleIfNeeded()
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
// MARK: - Exported (called from UserGistPush.mm)
|
|
73
88
|
|
|
74
89
|
@objc public func enablePush(options: [String: Any],
|
|
75
90
|
resolver: @escaping (Any?) -> Void,
|
|
76
91
|
rejecter: @escaping (String, String, Error?) -> Void) {
|
|
77
92
|
NSLog("[UserGistPush] enablePush called")
|
|
78
|
-
let
|
|
93
|
+
let expo = Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true
|
|
94
|
+
let mode = Bundle.main.object(forInfoDictionaryKey: "UserGistPushMode") as? String
|
|
95
|
+
if expo && mode == "disabled" {
|
|
96
|
+
resolver(["granted": false, "status": "not_determined", "platform": "ios", "error": "push_not_configured"])
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
let installDelegateProxy = mode == "expo-notifications" ? false : (options["installDelegateProxy"] as? Bool ?? true)
|
|
79
100
|
if installDelegateProxy {
|
|
80
101
|
Self.installUNDelegateSwizzleIfNeeded()
|
|
81
|
-
if let swizzler: AnyClass = NSClassFromString("UserGistPushSwizzle") {
|
|
102
|
+
if !expo, let swizzler: AnyClass = NSClassFromString("UserGistPushSwizzle") {
|
|
82
103
|
let selector = NSSelectorFromString("installIfNeeded")
|
|
83
104
|
let target = swizzler as AnyObject
|
|
84
105
|
if target.responds(to: selector) {
|
|
@@ -118,7 +139,10 @@ public final class UserGistPushImpl: NSObject {
|
|
|
118
139
|
@objc public func disablePush(resolver: @escaping (Any?) -> Void,
|
|
119
140
|
rejecter: @escaping (String, String, Error?) -> Void) {
|
|
120
141
|
DispatchQueue.main.async {
|
|
121
|
-
|
|
142
|
+
UserGistPushState.disable()
|
|
143
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool != true {
|
|
144
|
+
UIApplication.shared.unregisterForRemoteNotifications()
|
|
145
|
+
}
|
|
122
146
|
self.stateLock.lock()
|
|
123
147
|
self.lastApnsTokenHex = nil
|
|
124
148
|
self.stateLock.unlock()
|
|
@@ -148,6 +172,7 @@ public final class UserGistPushImpl: NSObject {
|
|
|
148
172
|
DispatchQueue.main.async {
|
|
149
173
|
let center = UNUserNotificationCenter.current()
|
|
150
174
|
let n = Int(count)
|
|
175
|
+
_ = UserGistPushState.updateBadge(count: n)
|
|
151
176
|
if #available(iOS 16.0, *) {
|
|
152
177
|
center.setBadgeCount(n) { error in
|
|
153
178
|
if let error = error {
|
|
@@ -190,11 +215,23 @@ public final class UserGistPushImpl: NSObject {
|
|
|
190
215
|
}
|
|
191
216
|
|
|
192
217
|
func recordNotificationReceived(userInfo: [AnyHashable: Any]) {
|
|
218
|
+
guard userInfo["usergist"] != nil else { return }
|
|
219
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled { return }
|
|
193
220
|
let normalized = normalizeUserInfo(userInfo)
|
|
194
221
|
emit(name: "UserGistPush:notificationReceived", body: normalized)
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
func recordNotificationOpened(userInfo: [AnyHashable: Any], actionIdentifier: String?) {
|
|
225
|
+
guard userInfo["usergist"] != nil else { return }
|
|
226
|
+
if Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled { return }
|
|
227
|
+
if let scope = userInfo["usergist_anonymous_id"] as? String, scope != UserGistPushState.snapshot["anonymousId"] as? String { return }
|
|
228
|
+
if actionIdentifier == UNNotificationDismissActionIdentifier {
|
|
229
|
+
if let payload = userInfo["usergist"] as? [String: Any], let id = payload["deliveryId"] as? String {
|
|
230
|
+
UserGistPushState.receipt("dismissed", id: id)
|
|
231
|
+
}
|
|
232
|
+
emit(name: "UserGistPush:notificationDismissed", body: normalizeUserInfo(userInfo))
|
|
233
|
+
return
|
|
234
|
+
}
|
|
198
235
|
var body = normalizeUserInfo(userInfo)
|
|
199
236
|
if let actionIdentifier = actionIdentifier, actionIdentifier != "com.apple.UNNotificationDefaultActionIdentifier" {
|
|
200
237
|
body["actionIdentifier"] = actionIdentifier
|
|
@@ -307,7 +344,7 @@ public final class UserGistPushImpl: NSObject {
|
|
|
307
344
|
NotificationCenter.default.addObserver(
|
|
308
345
|
forName: UIApplication.didBecomeActiveNotification,
|
|
309
346
|
object: nil,
|
|
310
|
-
queue: .main
|
|
347
|
+
queue: .main
|
|
311
348
|
) { _ in
|
|
312
349
|
UserGistUNDelegateSwizzler.reinstallIfHijacked()
|
|
313
350
|
}
|
|
@@ -368,12 +405,17 @@ public final class UserGistPushImpl: NSObject {
|
|
|
368
405
|
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
369
406
|
willPresent notification: UNNotification,
|
|
370
407
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
408
|
+
if notification.request.content.userInfo["usergist"] != nil && Bundle.main.object(forInfoDictionaryKey: "UserGistExpo") as? Bool == true && !UserGistPushState.enabled {
|
|
409
|
+
completionHandler([])
|
|
410
|
+
return
|
|
411
|
+
}
|
|
371
412
|
UserGistPushImpl.shared.recordNotificationReceived(userInfo: notification.request.content.userInfo)
|
|
372
413
|
if let existing = chainTarget(),
|
|
373
414
|
existing.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))) {
|
|
374
415
|
existing.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler)
|
|
375
416
|
return
|
|
376
417
|
}
|
|
418
|
+
if notification.request.content.userInfo["usergist"] == nil { completionHandler([]); return }
|
|
377
419
|
if #available(iOS 14.0, *) {
|
|
378
420
|
completionHandler([.banner, .list, .sound, .badge])
|
|
379
421
|
} else {
|