@usergist/feedback-react-native 0.1.0 → 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/EXPO-ACCEPTANCE.md +57 -0
  3. package/EXPO.md +103 -0
  4. package/README.md +10 -2
  5. package/UserGistFeedback.podspec +4 -1
  6. package/UserGistFeedbackExtension.podspec +1 -1
  7. package/android/build.gradle +3 -1
  8. package/android/src/main/AndroidManifest.xml +1 -0
  9. package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +78 -12
  10. package/android/src/main/java/studio/usergist/feedback/UserGistNotificationDismissReceiver.kt +17 -0
  11. package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +46 -5
  12. package/android/src/main/java/studio/usergist/feedback/UserGistPushState.kt +69 -0
  13. package/app.plugin.js +2 -0
  14. package/dist/NativeUserGistPush.d.ts +10 -0
  15. package/dist/NativeUserGistPush.d.ts.map +1 -1
  16. package/dist/NativeUserGistPush.js +6 -10
  17. package/dist/NativeUserGistPush.js.map +1 -1
  18. package/dist/Push.d.ts.map +1 -1
  19. package/dist/Push.js +9 -0
  20. package/dist/Push.js.map +1 -1
  21. package/dist/UserGist.d.ts +1 -0
  22. package/dist/UserGist.d.ts.map +1 -1
  23. package/dist/UserGist.js +218 -79
  24. package/dist/UserGist.js.map +1 -1
  25. package/dist/expo/index.d.ts +68 -0
  26. package/dist/expo/index.d.ts.map +1 -0
  27. package/dist/expo/index.js +120 -0
  28. package/dist/expo/index.js.map +1 -0
  29. package/dist/internal/consent.d.ts.map +1 -1
  30. package/dist/internal/consent.js +4 -1
  31. package/dist/internal/consent.js.map +1 -1
  32. package/dist/internal/context.d.ts +1 -1
  33. package/dist/internal/context.d.ts.map +1 -1
  34. package/dist/internal/context.js +1 -1
  35. package/dist/internal/context.js.map +1 -1
  36. package/dist/internal/engine.d.ts.map +1 -1
  37. package/dist/internal/engine.js +6 -1
  38. package/dist/internal/engine.js.map +1 -1
  39. package/dist/internal/events.d.ts +1 -0
  40. package/dist/internal/events.d.ts.map +1 -1
  41. package/dist/internal/events.js +6 -0
  42. package/dist/internal/events.js.map +1 -1
  43. package/dist/internal/push-dedupe.d.ts +3 -0
  44. package/dist/internal/push-dedupe.d.ts.map +1 -0
  45. package/dist/internal/push-dedupe.js +33 -0
  46. package/dist/internal/push-dedupe.js.map +1 -0
  47. package/dist/internal/storage.d.ts +1 -0
  48. package/dist/internal/storage.d.ts.map +1 -1
  49. package/dist/internal/storage.js +1 -0
  50. package/dist/internal/storage.js.map +1 -1
  51. package/dist/internal/transport.d.ts +5 -1
  52. package/dist/internal/transport.d.ts.map +1 -1
  53. package/dist/internal/transport.js +2 -2
  54. package/dist/internal/transport.js.map +1 -1
  55. package/dist/native/events.d.ts +2 -0
  56. package/dist/native/events.d.ts.map +1 -1
  57. package/dist/native/events.js +2 -0
  58. package/dist/native/events.js.map +1 -1
  59. package/dist/native/push-adapter.d.ts +11 -0
  60. package/dist/native/push-adapter.d.ts.map +1 -0
  61. package/dist/native/push-adapter.js +10 -0
  62. package/dist/native/push-adapter.js.map +1 -0
  63. package/dist/native/push-bridge.d.ts +9 -0
  64. package/dist/native/push-bridge.d.ts.map +1 -1
  65. package/dist/native/push-bridge.js +39 -4
  66. package/dist/native/push-bridge.js.map +1 -1
  67. package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
  68. package/dist/ui/requests/RequestsHost.js +8 -0
  69. package/dist/ui/requests/RequestsHost.js.map +1 -1
  70. package/expo-module.config.json +8 -0
  71. package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
  72. package/expo-support/UserGistFeedbackExpo.podspec +17 -0
  73. package/ios/Extension/UserGistNotificationService.swift +52 -5
  74. package/ios/Shared/UserGistNotificationActions.swift +27 -0
  75. package/ios/Shared/UserGistPushState.swift +110 -0
  76. package/ios/UserGistPush.h +1 -6
  77. package/ios/UserGistPush.mm +18 -0
  78. package/ios/UserGistPushImpl.swift +46 -4
  79. package/ios/UserGistPushInterop.h +5 -0
  80. package/package.json +31 -13
  81. package/plugin/index.cjs +269 -0
  82. package/plugin/index.d.cts +14 -0
  83. 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.1.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,14 +88,12 @@
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"
73
93
  },
74
- "codegenConfig": {
75
- "name": "RNUserGistPushSpec",
76
- "type": "modules",
77
- "jsSrcsDir": "src",
78
- "android": {
79
- "javaPackageName": "studio.usergist.feedback"
94
+ "peerDependenciesMeta": {
95
+ "expo": {
96
+ "optional": true
80
97
  }
81
98
  },
82
99
  "scripts": {
@@ -84,6 +101,7 @@
84
101
  "dev": "tsc --watch",
85
102
  "test": "vitest run",
86
103
  "typecheck": "tsc --noEmit",
87
- "pack:check": "pnpm publish --dry-run --no-git-checks"
104
+ "pack:check": "node scripts/check-package.mjs",
105
+ "test:plugin": "node --test plugin/*.test.cjs"
88
106
  }
89
107
  }
@@ -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
+ }