@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usergist/feedback-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"description": "userGist feedback + push SDK for React Native (self-contained, no Firebase/OneSignal/WonderPush required)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://usergist.com/docs/sdks/react-native",
|
|
@@ -35,7 +35,16 @@
|
|
|
35
35
|
"import": "./dist/preview/index.js",
|
|
36
36
|
"default": "./dist/preview/index.js"
|
|
37
37
|
},
|
|
38
|
-
"./package.json": "./package.json"
|
|
38
|
+
"./package.json": "./package.json",
|
|
39
|
+
"./expo": {
|
|
40
|
+
"types": "./dist/expo/index.d.ts",
|
|
41
|
+
"import": "./dist/expo/index.js",
|
|
42
|
+
"default": "./dist/expo/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./app.plugin.js": {
|
|
45
|
+
"types": "./plugin/index.d.cts",
|
|
46
|
+
"default": "./plugin/index.cjs"
|
|
47
|
+
}
|
|
39
48
|
},
|
|
40
49
|
"files": [
|
|
41
50
|
"dist",
|
|
@@ -49,19 +58,29 @@
|
|
|
49
58
|
"react-native.config.js",
|
|
50
59
|
"README.md",
|
|
51
60
|
"CHANGELOG.md",
|
|
52
|
-
"LICENSE"
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"app.plugin.js",
|
|
63
|
+
"plugin",
|
|
64
|
+
"expo-module.config.json",
|
|
65
|
+
"expo-support",
|
|
66
|
+
"EXPO.md",
|
|
67
|
+
"EXPO-ACCEPTANCE.md",
|
|
68
|
+
"!plugin/*.test.cjs"
|
|
53
69
|
],
|
|
54
70
|
"peerDependencies": {
|
|
55
71
|
"@react-native-async-storage/async-storage": ">=1.19",
|
|
56
72
|
"react": ">=18",
|
|
57
73
|
"react-native": ">=0.72",
|
|
58
|
-
"react-native-safe-area-context": ">=4"
|
|
74
|
+
"react-native-safe-area-context": ">=4",
|
|
75
|
+
"expo": ">=56 <58"
|
|
59
76
|
},
|
|
60
77
|
"dependencies": {
|
|
78
|
+
"@expo/plist": "0.4.7",
|
|
61
79
|
"@usergist/sdk-core": "^0.1.0"
|
|
62
80
|
},
|
|
63
81
|
"publishConfig": {
|
|
64
|
-
"access": "public"
|
|
82
|
+
"access": "public",
|
|
83
|
+
"tag": "next"
|
|
65
84
|
},
|
|
66
85
|
"devDependencies": {
|
|
67
86
|
"@types/react": "^18.3.3",
|
|
@@ -69,13 +88,20 @@
|
|
|
69
88
|
"react-native": "0.74.0",
|
|
70
89
|
"react-native-safe-area-context": "^4",
|
|
71
90
|
"vitest": "^2.1.0",
|
|
72
|
-
"typescript": "^5.5.4"
|
|
91
|
+
"typescript": "^5.5.4",
|
|
92
|
+
"expo": "57.0.20"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"expo": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
73
98
|
},
|
|
74
99
|
"scripts": {
|
|
75
100
|
"build": "tsc",
|
|
76
101
|
"dev": "tsc --watch",
|
|
77
102
|
"test": "vitest run",
|
|
78
103
|
"typecheck": "tsc --noEmit",
|
|
79
|
-
"pack:check": "node scripts/check-package.mjs"
|
|
104
|
+
"pack:check": "node scripts/check-package.mjs",
|
|
105
|
+
"test:plugin": "node --test plugin/*.test.cjs"
|
|
80
106
|
}
|
|
81
107
|
}
|
package/plugin/index.cjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { createHash } = require('node:crypto')
|
|
5
|
+
const {
|
|
6
|
+
createRunOncePlugin, withInfoPlist, withEntitlementsPlist, withXcodeProject,
|
|
7
|
+
withPodfile, withAndroidManifest, withAppBuildGradle, withProjectBuildGradle,
|
|
8
|
+
withDangerousMod,
|
|
9
|
+
} = require('expo/config-plugins')
|
|
10
|
+
const { version } = require('../package.json')
|
|
11
|
+
const marker = '// @generated by usergist'
|
|
12
|
+
const fail = message => { throw new Error(`[UserGist Expo] ${message}`) }
|
|
13
|
+
const unique = values => [...new Set(values)]
|
|
14
|
+
const identifier = (value, label) => {
|
|
15
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(value)) fail(`Invalid ${label}: ${value}`)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function settings(config, options = {}) {
|
|
20
|
+
const push = options.push
|
|
21
|
+
if (!push) return null
|
|
22
|
+
if (!['automatic', 'expo-notifications'].includes(push.mode)) fail('push.mode must be automatic or expo-notifications')
|
|
23
|
+
const result = { ...push }
|
|
24
|
+
if (config.platforms?.includes('ios') !== false) {
|
|
25
|
+
const bundle = config.ios?.bundleIdentifier
|
|
26
|
+
if (!bundle) fail('Set ios.bundleIdentifier before configuring push')
|
|
27
|
+
if (!['development', 'production'].includes(push.ios?.apsEnvironment)) {
|
|
28
|
+
fail('Set push.ios.apsEnvironment from the signing profile (development or production), not the UserGist data environment')
|
|
29
|
+
}
|
|
30
|
+
const target = push.ios.extensionTargetName || 'UserGistNotificationServiceExtension'
|
|
31
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(target)) fail('extensionTargetName must be a valid Swift/Xcode identifier')
|
|
32
|
+
result.ios = {
|
|
33
|
+
...push.ios, extensionTargetName: target,
|
|
34
|
+
appGroupIdentifier: identifier(push.ios.appGroupIdentifier || `group.${bundle}.usergist`, 'App Group'),
|
|
35
|
+
extensionBundleIdentifier: identifier(push.ios.extensionBundleIdentifier || `${bundle}.${target}`, 'extension bundle identifier'),
|
|
36
|
+
}
|
|
37
|
+
if (!result.ios.extensionBundleIdentifier.startsWith(`${bundle}.`)) fail('The extension bundle identifier must be prefixed by ios.bundleIdentifier')
|
|
38
|
+
}
|
|
39
|
+
if (push.android?.notificationColor && !/^#[0-9a-f]{6}$/i.test(push.android.notificationColor)) fail('notificationColor must be #RRGGBB')
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeOwned(file, content, ownedTarget = false) {
|
|
44
|
+
if (fs.existsSync(file) && !ownedTarget && !fs.readFileSync(file, 'utf8').includes(marker)) fail(`Existing file is not owned by UserGist: ${file}. See /docs/sdks/expo#conflicts`)
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
46
|
+
fs.writeFileSync(file, content)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertNotificationOwnership(push, projectRoot) {
|
|
50
|
+
if (push?.mode !== 'automatic') return
|
|
51
|
+
let installed = false
|
|
52
|
+
try { require.resolve('expo-notifications/package.json', { paths: [projectRoot] }); installed = true } catch {}
|
|
53
|
+
if (installed) fail('expo-notifications is installed and autolinks native notification handlers. Select push.mode="expo-notifications" and add its plugin to preserve notification ownership')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function withUserGist(config, options = {}) {
|
|
57
|
+
const push = settings(config, options)
|
|
58
|
+
const plugins = (config.plugins || []).map(p => Array.isArray(p) ? p[0] : p)
|
|
59
|
+
if (push?.mode === 'automatic' && plugins.includes('expo-notifications')) {
|
|
60
|
+
fail('expo-notifications is configured. Select push.mode="expo-notifications" to preserve its handlers')
|
|
61
|
+
}
|
|
62
|
+
if (push?.mode === 'expo-notifications' && !plugins.includes('expo-notifications')) {
|
|
63
|
+
fail('Install expo-notifications and add its plugin before the UserGist plugin')
|
|
64
|
+
}
|
|
65
|
+
config = withInfoPlist(config, c => {
|
|
66
|
+
assertNotificationOwnership(push, c.modRequest.projectRoot)
|
|
67
|
+
c.modResults.UserGistExpo = true
|
|
68
|
+
c.modResults.UserGistPushMode = push?.mode || 'disabled'
|
|
69
|
+
if (push?.ios) {
|
|
70
|
+
c.modResults.UserGistAppGroup = push.ios.appGroupIdentifier
|
|
71
|
+
c.modResults.UserGistApsEnvironment = push.ios.apsEnvironment
|
|
72
|
+
c.modResults.UIBackgroundModes = unique([...(c.modResults.UIBackgroundModes || []), 'remote-notification'])
|
|
73
|
+
}
|
|
74
|
+
return c
|
|
75
|
+
})
|
|
76
|
+
config = withAndroidManifest(config, c => {
|
|
77
|
+
const manifest = c.modResults.manifest
|
|
78
|
+
manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools'
|
|
79
|
+
const app = manifest.application[0]
|
|
80
|
+
app['meta-data'] ||= []
|
|
81
|
+
for (const [name, value] of [['UserGistExpo', 'true'], ['UserGistPushMode', push?.mode || 'disabled']]) {
|
|
82
|
+
const old = app['meta-data'].find(m => m.$['android:name'] === name)
|
|
83
|
+
if (old) old.$['android:value'] = value
|
|
84
|
+
else app['meta-data'].push({ $: { 'android:name': name, 'android:value': value } })
|
|
85
|
+
}
|
|
86
|
+
const services = app.service ||= []
|
|
87
|
+
const dispatcher = 'studio.usergist.feedback.expo.UserGistExpoMessagingService'
|
|
88
|
+
const owns = new Set([dispatcher, 'studio.usergist.feedback.UserGistFirebaseMessagingService', 'expo.modules.notifications.service.ExpoFirebaseMessagingService'])
|
|
89
|
+
for (const service of services) {
|
|
90
|
+
if (push && (service['intent-filter'] || []).some(f => (f.action || []).some(a => a.$['android:name'] === 'com.google.firebase.MESSAGING_EVENT')) && !owns.has(service.$['android:name'])) {
|
|
91
|
+
fail(`Custom FCM service ${service.$['android:name']} requires explicit host integration; see /docs/sdks/expo#conflicts`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const setService = (name, enabled) => {
|
|
95
|
+
let service = services.find(s => s.$['android:name'] === name)
|
|
96
|
+
if (!service) services.push(service = { $: { 'android:name': name } })
|
|
97
|
+
Object.assign(service.$, { 'android:enabled': String(enabled), 'tools:replace': 'android:enabled' })
|
|
98
|
+
}
|
|
99
|
+
setService('studio.usergist.feedback.UserGistFirebaseMessagingService', push?.mode === 'automatic')
|
|
100
|
+
if (push?.mode === 'expo-notifications') {
|
|
101
|
+
setService('expo.modules.notifications.service.ExpoFirebaseMessagingService', false)
|
|
102
|
+
let service = services.find(s => s.$['android:name'] === dispatcher)
|
|
103
|
+
if (!service) services.push(service = { $: { 'android:name': dispatcher } })
|
|
104
|
+
Object.assign(service.$, { 'android:exported': 'false', 'android:enabled': 'true' })
|
|
105
|
+
service['intent-filter'] = [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
|
|
106
|
+
} else if (services.some(s => s.$['android:name'] === dispatcher)) {
|
|
107
|
+
fail('Changing push ownership in an existing native project requires a fresh generated project; see /docs/sdks/expo#conflicts')
|
|
108
|
+
}
|
|
109
|
+
if (push?.android?.notificationIcon) {
|
|
110
|
+
app['meta-data'].push({ $: { 'android:name': 'UserGistNotificationIcon', 'android:resource': '@drawable/usergist_notification_icon' } })
|
|
111
|
+
app['meta-data'] = app['meta-data'].filter((m, i, a) => a.findIndex(n => n.$['android:name'] === m.$['android:name']) === i)
|
|
112
|
+
}
|
|
113
|
+
if (push?.android?.notificationColor) {
|
|
114
|
+
const name = 'UserGistNotificationColor'
|
|
115
|
+
app['meta-data'] = app['meta-data'].filter(m => m.$['android:name'] !== name)
|
|
116
|
+
app['meta-data'].push({ $: { 'android:name': name, 'android:value': push.android.notificationColor } })
|
|
117
|
+
}
|
|
118
|
+
return c
|
|
119
|
+
})
|
|
120
|
+
if (!push) return config
|
|
121
|
+
|
|
122
|
+
if (config.platforms?.includes('android') !== false) {
|
|
123
|
+
if (!config.android?.googleServicesFile) fail('Set android.googleServicesFile to your Firebase client google-services.json (not a service-account key)')
|
|
124
|
+
config = withProjectBuildGradle(config, c => {
|
|
125
|
+
if (c.modResults.language && c.modResults.language !== 'groovy') fail('Kotlin Gradle configuration requires explicit Google Services integration; see /docs/sdks/expo#conflicts')
|
|
126
|
+
if (!c.modResults.contents.includes('com.google.gms:google-services:')) {
|
|
127
|
+
if (!/dependencies\s*\{/.test(c.modResults.contents)) fail('Unsupported project Gradle format; add the Google Services classpath explicitly')
|
|
128
|
+
c.modResults.contents = c.modResults.contents.replace(/dependencies\s*\{/, "dependencies {\n classpath('com.google.gms:google-services:4.4.4')")
|
|
129
|
+
}
|
|
130
|
+
return c
|
|
131
|
+
})
|
|
132
|
+
config = withAppBuildGradle(config, c => {
|
|
133
|
+
if (c.modResults.language && c.modResults.language !== 'groovy') fail('Kotlin Gradle configuration requires an explicit notification dispatcher dependency; see /docs/sdks/expo#conflicts')
|
|
134
|
+
if (!/apply plugin: ['"]com.google.gms.google-services['"]/.test(c.modResults.contents)) c.modResults.contents += '\napply plugin: "com.google.gms.google-services"\n'
|
|
135
|
+
if (push.mode === 'expo-notifications') {
|
|
136
|
+
const folder = path.dirname(require.resolve('expo-notifications/package.json', { paths: [c.modRequest.projectRoot] }))
|
|
137
|
+
const publication = JSON.parse(fs.readFileSync(path.join(folder, 'expo-module.config.json'), 'utf8')).android?.publication
|
|
138
|
+
const fallback = publication ? `implementation("${publication.groupId}:${publication.artifactId}:${publication.version}")` : `throw new GradleException("expo-notifications is not autolinked")`
|
|
139
|
+
const stanza = `// usergist-expo-dispatch\ndependencies { if (findProject(":expo-notifications") != null) { implementation project(":expo-notifications") } else { ${fallback} } }\n// usergist-expo-dispatch-end`
|
|
140
|
+
if (c.modResults.contents.includes('// usergist-expo-dispatch-end')) c.modResults.contents = c.modResults.contents.replace(/\/\/ usergist-expo-dispatch[\s\S]*?\/\/ usergist-expo-dispatch-end/, stanza)
|
|
141
|
+
else if (c.modResults.contents.includes('// usergist-expo-dispatch')) c.modResults.contents = c.modResults.contents.replace(/\/\/ usergist-expo-dispatch\ndependencies \{ implementation project\(":expo-notifications"\) \}/, stanza)
|
|
142
|
+
else c.modResults.contents += `\n${stanza}\n`
|
|
143
|
+
}
|
|
144
|
+
return c
|
|
145
|
+
})
|
|
146
|
+
config = withDangerousMod(config, ['android', async c => {
|
|
147
|
+
const root = c.modRequest.platformProjectRoot
|
|
148
|
+
assertNotificationOwnership(push, c.modRequest.projectRoot)
|
|
149
|
+
const google = path.resolve(c.modRequest.projectRoot, c.android.googleServicesFile)
|
|
150
|
+
const json = JSON.parse(fs.readFileSync(google, 'utf8'))
|
|
151
|
+
if (!json.client?.some(client => client.client_info?.android_client_info?.package_name === c.android.package)) fail('google-services.json does not contain android.package')
|
|
152
|
+
if (push.mode === 'expo-notifications') {
|
|
153
|
+
const template = fs.readFileSync(path.join(__dirname, 'templates/UserGistExpoMessagingService.kt'), 'utf8')
|
|
154
|
+
writeOwned(path.join(root, 'app/src/main/java/studio/usergist/feedback/expo/UserGistExpoMessagingService.kt'), template)
|
|
155
|
+
}
|
|
156
|
+
if (push.android?.notificationIcon) {
|
|
157
|
+
const file = path.resolve(c.modRequest.projectRoot, push.android.notificationIcon)
|
|
158
|
+
if (path.extname(file).toLowerCase() !== '.png') fail('notificationIcon must be a white-on-transparent PNG')
|
|
159
|
+
const dest = path.join(root, 'app/src/main/res/drawable/usergist_notification_icon.png')
|
|
160
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
161
|
+
const ownershipFile = path.join(root, '.usergist-expo-assets.json')
|
|
162
|
+
const ownership = fs.existsSync(ownershipFile) ? JSON.parse(fs.readFileSync(ownershipFile, 'utf8')) : { owner: '@usergist/feedback-react-native', files: {} }
|
|
163
|
+
if (ownership.owner !== '@usergist/feedback-react-native') fail(`Asset ownership file is not owned by UserGist: ${ownershipFile}`)
|
|
164
|
+
const asset = path.relative(root, dest)
|
|
165
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
|
|
166
|
+
if (fs.existsSync(dest) && !fs.readFileSync(dest).equals(fs.readFileSync(file)) && ownership.files[asset] !== hash(fs.readFileSync(dest))) fail('Notification icon was customized outside the plugin; preserve it and follow /docs/sdks/expo#conflicts')
|
|
167
|
+
fs.copyFileSync(file, dest)
|
|
168
|
+
ownership.files[asset] = hash(fs.readFileSync(dest))
|
|
169
|
+
fs.writeFileSync(ownershipFile, JSON.stringify(ownership))
|
|
170
|
+
}
|
|
171
|
+
return c
|
|
172
|
+
}])
|
|
173
|
+
}
|
|
174
|
+
if (push.ios) config = withExtension(config, push.ios)
|
|
175
|
+
return config
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function withExtension(config, ios) {
|
|
179
|
+
const target = ios.extensionTargetName
|
|
180
|
+
const group = ios.appGroupIdentifier
|
|
181
|
+
const entry = { targetName: target, bundleIdentifier: ios.extensionBundleIdentifier, entitlements: { 'com.apple.security.application-groups': [group] } }
|
|
182
|
+
config.extra ||= {}; config.extra.eas ||= {}; config.extra.eas.build ||= {}
|
|
183
|
+
config.extra.eas.build.experimental ||= {}; config.extra.eas.build.experimental.ios ||= {}
|
|
184
|
+
const extensions = config.extra.eas.build.experimental.ios.appExtensions ||= []
|
|
185
|
+
const old = extensions.find(e => e.targetName === target || e.bundleIdentifier === entry.bundleIdentifier)
|
|
186
|
+
if (old && JSON.stringify(old) !== JSON.stringify(entry)) fail('Conflicting EAS extension configuration; preserve the existing extension and follow /docs/sdks/expo#conflicts')
|
|
187
|
+
if (!old) extensions.push(entry)
|
|
188
|
+
config = withEntitlementsPlist(config, c => {
|
|
189
|
+
c.modResults['aps-environment'] = ios.apsEnvironment
|
|
190
|
+
c.modResults['com.apple.security.application-groups'] = unique([...(c.modResults['com.apple.security.application-groups'] || []), group])
|
|
191
|
+
return c
|
|
192
|
+
})
|
|
193
|
+
config = withPodfile(config, c => {
|
|
194
|
+
const begin = '# usergist-extension-begin'
|
|
195
|
+
const stanza = `${begin}\ntarget '${target}' do\n pod 'UserGistFeedbackExtension', :path => File.dirname(\`node --print "require.resolve('@usergist/feedback-react-native/package.json')"\`.strip)\nend\n# usergist-extension-end`
|
|
196
|
+
if (c.modResults.contents.includes(begin)) c.modResults.contents = c.modResults.contents.replace(/# usergist-extension-begin[\s\S]*?# usergist-extension-end/, stanza)
|
|
197
|
+
else {
|
|
198
|
+
if (c.modResults.contents.includes(`target '${target}'`) || c.modResults.contents.includes(`target "${target}"`)) fail('Existing extension Podfile target is not owned by UserGist')
|
|
199
|
+
c.modResults.contents += `\n${stanza}\n`
|
|
200
|
+
}
|
|
201
|
+
return c
|
|
202
|
+
})
|
|
203
|
+
config = withXcodeProject(config, c => {
|
|
204
|
+
const project = c.modResults
|
|
205
|
+
const root = c.modRequest.platformProjectRoot
|
|
206
|
+
const folder = path.join(root, target)
|
|
207
|
+
const swift = path.join(folder, 'NotificationService.swift')
|
|
208
|
+
const ownsFolder = fs.existsSync(swift) && fs.readFileSync(swift, 'utf8').includes(marker)
|
|
209
|
+
const targets = project.pbxNativeTargetSection()
|
|
210
|
+
for (const [key, native] of Object.entries(targets)) {
|
|
211
|
+
if (key.endsWith('_comment') || String(native.name).replaceAll('"', '') === target) continue
|
|
212
|
+
if (String(native.productType).includes('app-extension')) {
|
|
213
|
+
const list = project.pbxXCConfigurationList()[native.buildConfigurationList]
|
|
214
|
+
for (const ref of list?.buildConfigurations || []) {
|
|
215
|
+
const settings = project.pbxXCBuildConfigurationSection()[ref.value].buildSettings
|
|
216
|
+
const name = String(settings.INFOPLIST_FILE || '').replaceAll('"', '').replace(/\$\((?:SRCROOT|PROJECT_DIR)\)|\$\{(?:SRCROOT|PROJECT_DIR)\}/g, root).replace(/\$\(TARGET_NAME\)/g, String(native.name).replaceAll('"', ''))
|
|
217
|
+
const file = path.resolve(root, name)
|
|
218
|
+
if (!name || name.includes('$') || !fs.existsSync(file)) fail(`Cannot inspect Info.plist for existing app extension ${native.name}; verify notification ownership explicitly, see /docs/sdks/expo#conflicts`)
|
|
219
|
+
if (name && fs.existsSync(file) && fs.readFileSync(file, 'utf8').includes('com.apple.usernotifications.service')) fail(`Existing notification service extension ${native.name} needs manual composition; see /docs/sdks/expo#conflicts`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let found = Object.entries(targets).find(([key, t]) => !key.endsWith('_comment') && String(t.name).replaceAll('"', '') === target)
|
|
224
|
+
if (found && (!fs.existsSync(swift) || !fs.readFileSync(swift, 'utf8').includes(marker))) fail(`Xcode target ${target} already exists and is not owned by UserGist`)
|
|
225
|
+
// Xcode project generation runs after the standard Expo native template exists.
|
|
226
|
+
writeOwned(swift, `${marker}\nimport UserGistFeedbackExtension\nclass NotificationService: UserGistNotificationService {}\n`)
|
|
227
|
+
const plist = require('@expo/plist').default
|
|
228
|
+
const info = { CFBundleDisplayName: target, CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)', CFBundleExecutable: '$(EXECUTABLE_NAME)', CFBundleName: '$(PRODUCT_NAME)', CFBundlePackageType: 'XPC!', CFBundleShortVersionString: c.version || '1.0.0', CFBundleVersion: c.ios?.buildNumber || '1', UserGistExpo: true, UserGistAppGroup: group, NSExtension: { NSExtensionPointIdentifier: 'com.apple.usernotifications.service', NSExtensionPrincipalClass: '$(PRODUCT_MODULE_NAME).NotificationService' } }
|
|
229
|
+
for (const [file, value] of [['Info.plist', info], [`${target}.entitlements`, entry.entitlements]]) {
|
|
230
|
+
const destination = path.join(folder, file)
|
|
231
|
+
const existing = ownsFolder && fs.existsSync(destination) ? plist.parse(fs.readFileSync(destination, 'utf8')) : {}
|
|
232
|
+
const merged = { ...existing, ...value }
|
|
233
|
+
if (value.NSExtension) merged.NSExtension = { ...existing.NSExtension, ...value.NSExtension }
|
|
234
|
+
if (value['com.apple.security.application-groups']) merged['com.apple.security.application-groups'] = unique([...(existing['com.apple.security.application-groups'] || []), group])
|
|
235
|
+
const xml = plist.build(merged).replace('<plist version="1.0">', `<!-- ${marker} -->\n<plist version="1.0">`)
|
|
236
|
+
writeOwned(destination, xml, ownsFolder)
|
|
237
|
+
}
|
|
238
|
+
project.hash.project.objects.PBXTargetDependency ||= {}
|
|
239
|
+
project.hash.project.objects.PBXContainerItemProxy ||= {}
|
|
240
|
+
if (!found) {
|
|
241
|
+
const native = project.addTarget(target, 'app_extension', target, ios.extensionBundleIdentifier)
|
|
242
|
+
const groupEntry = project.addPbxGroup([], target, target)
|
|
243
|
+
project.addToPbxGroup(groupEntry.uuid, project.getFirstProject().firstProject.mainGroup)
|
|
244
|
+
project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', native.uuid)
|
|
245
|
+
project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', native.uuid)
|
|
246
|
+
project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', native.uuid)
|
|
247
|
+
project.addSourceFile('NotificationService.swift', { target: native.uuid }, groupEntry.uuid)
|
|
248
|
+
// addTarget adds the embed phase and target dependency for app extensions.
|
|
249
|
+
found = [native.uuid, native.pbxNativeTarget]
|
|
250
|
+
}
|
|
251
|
+
const host = project.getFirstTarget()
|
|
252
|
+
const dependencies = project.hash.project.objects.PBXTargetDependency
|
|
253
|
+
if (!host.firstTarget.dependencies.some(ref => dependencies[ref.value]?.target === found[0])) project.addTargetDependency(host.uuid, [found[0]])
|
|
254
|
+
for (const ref of Object.values(project.pbxFileReferenceSection())) {
|
|
255
|
+
if (ref && typeof ref === 'object' && String(ref.path).replaceAll('"', '') === `${target}/NotificationService.swift`) ref.path = '"NotificationService.swift"'
|
|
256
|
+
}
|
|
257
|
+
const list = project.pbxXCConfigurationList()[found[1].buildConfigurationList]
|
|
258
|
+
for (const ref of list.buildConfigurations) {
|
|
259
|
+
const build = project.pbxXCBuildConfigurationSection()[ref.value].buildSettings
|
|
260
|
+
Object.assign(build, { INFOPLIST_FILE: `"${target}/Info.plist"`, CODE_SIGN_ENTITLEMENTS: `"${target}/${target}.entitlements"`, PRODUCT_BUNDLE_IDENTIFIER: `"${ios.extensionBundleIdentifier}"`, SWIFT_VERSION: '5.0', IPHONEOS_DEPLOYMENT_TARGET: '15.1', TARGETED_DEVICE_FAMILY: '"1,2"', SKIP_INSTALL: 'YES', APPLICATION_EXTENSION_API_ONLY: 'YES', GENERATE_INFOPLIST_FILE: 'NO' })
|
|
261
|
+
if (c.ios?.appleTeamId) build.DEVELOPMENT_TEAM = c.ios.appleTeamId
|
|
262
|
+
}
|
|
263
|
+
return c
|
|
264
|
+
})
|
|
265
|
+
return config
|
|
266
|
+
}
|
|
267
|
+
module.exports = createRunOncePlugin(withUserGist, '@usergist/feedback-react-native', version)
|
|
268
|
+
module.exports.withUserGist = withUserGist
|
|
269
|
+
module.exports.settings = settings
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface UserGistExpoPluginOptions {
|
|
2
|
+
push?: {
|
|
3
|
+
mode: 'automatic' | 'expo-notifications'
|
|
4
|
+
ios?: {
|
|
5
|
+
apsEnvironment: 'development' | 'production'
|
|
6
|
+
appGroupIdentifier?: string
|
|
7
|
+
extensionTargetName?: string
|
|
8
|
+
extensionBundleIdentifier?: string
|
|
9
|
+
}
|
|
10
|
+
android?: { notificationIcon?: string; notificationColor?: string }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
declare const plugin: import('expo/config-plugins').ConfigPlugin<UserGistExpoPluginOptions>
|
|
14
|
+
export default plugin
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @generated by usergist
|
|
2
|
+
package studio.usergist.feedback.expo
|
|
3
|
+
|
|
4
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
5
|
+
import expo.modules.notifications.service.ExpoFirebaseMessagingService
|
|
6
|
+
import studio.usergist.feedback.UserGistFirebaseMessagingService
|
|
7
|
+
|
|
8
|
+
class UserGistExpoMessagingService : ExpoFirebaseMessagingService() {
|
|
9
|
+
override fun onMessageReceived(message: RemoteMessage) {
|
|
10
|
+
if (message.data.containsKey("usergist_campaign_id") || message.data["usergist_silent"] == "1") {
|
|
11
|
+
UserGistFirebaseMessagingService.handleMessage(this, message)
|
|
12
|
+
} else {
|
|
13
|
+
super.onMessageReceived(message)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
override fun onNewToken(token: String) {
|
|
17
|
+
super.onNewToken(token)
|
|
18
|
+
UserGistFirebaseMessagingService.handleToken(token)
|
|
19
|
+
}
|
|
20
|
+
}
|