sitepong 0.2.2 → 0.2.4

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 (57) hide show
  1. package/android/build.gradle +39 -0
  2. package/android/src/main/java/com/sitepong/deviceid/DeviceIdHelper.kt +45 -0
  3. package/android/src/main/java/com/sitepong/deviceid/DeviceIdModule.kt +45 -0
  4. package/android/src/main/java/com/sitepong/screenrecorder/ChunkUploader.kt +78 -0
  5. package/android/src/main/java/com/sitepong/screenrecorder/H264Encoder.kt +163 -0
  6. package/android/src/main/java/com/sitepong/screenrecorder/ScreenCapture.kt +103 -0
  7. package/android/src/main/java/com/sitepong/screenrecorder/ScreenRecorderModule.kt +80 -0
  8. package/android/src/main/java/com/sitepong/screenrecorder/SensitiveViewManager.kt +19 -0
  9. package/app.plugin.js +426 -0
  10. package/dist/cdn/sitepong.min.js +5 -5
  11. package/dist/cdn/sitepong.min.js.map +1 -1
  12. package/dist/entries/rn.d.ts +188 -16
  13. package/dist/entries/rn.js +188 -35
  14. package/dist/entries/rn.js.map +1 -1
  15. package/dist/entries/web.js +2 -1
  16. package/dist/entries/web.js.map +1 -1
  17. package/dist/entries/web.mjs +2 -1
  18. package/dist/entries/web.mjs.map +1 -1
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/react/index.js +2 -1
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/index.mjs +2 -1
  28. package/dist/react/index.mjs.map +1 -1
  29. package/expo-module.config.json +16 -0
  30. package/ios/DeviceId/DeviceIdModule.swift +55 -0
  31. package/ios/DeviceId/KeychainHelper.swift +68 -0
  32. package/ios/LiveActivity/ConfigStore.swift +43 -0
  33. package/ios/LiveActivity/DSLNode.swift +188 -0
  34. package/ios/LiveActivity/SitePongActivityAttributes.swift +112 -0
  35. package/ios/LiveActivity/SitePongLiveActivityModule.swift +250 -0
  36. package/ios/LiveActivity/widget/ColorHex.swift +30 -0
  37. package/ios/LiveActivity/widget/DSLAnimations.swift +128 -0
  38. package/ios/LiveActivity/widget/DSLRenderer.swift +538 -0
  39. package/ios/LiveActivity/widget/SitePongLiveActivityWidget.swift +393 -0
  40. package/ios/LiveActivity/widget/SitePongWidgetBundle.swift +84 -0
  41. package/ios/ScreenRecorder/ChunkUploader.swift +52 -0
  42. package/ios/ScreenRecorder/H264Encoder.swift +229 -0
  43. package/ios/ScreenRecorder/ScreenCapture.swift +149 -0
  44. package/ios/ScreenRecorder/ScreenRecorderModule.swift +179 -0
  45. package/ios/ScreenRecorder/SensitiveViewManager.swift +63 -0
  46. package/ios/Sitepong.podspec +57 -0
  47. package/ios/WatchtowerCore/Screenshotter.swift +163 -0
  48. package/ios/WatchtowerCore/SwiftUIModifiers.swift +78 -0
  49. package/ios/WatchtowerCore/Swizzling.swift +59 -0
  50. package/ios/WatchtowerCore/TapEvent.swift +61 -0
  51. package/ios/WatchtowerCore/Transport.swift +136 -0
  52. package/ios/WatchtowerCore/UIViewExtensions.swift +31 -0
  53. package/ios/WatchtowerCore/Watchtower.swift +80 -0
  54. package/ios/WatchtowerCore/WatchtowerEngine.swift +565 -0
  55. package/ios/WatchtowerCore/WatchtowerHash.swift +120 -0
  56. package/ios/WatchtowerCore/WatchtowerRegistry.swift +87 -0
  57. package/package.json +19 -8
package/app.plugin.js ADDED
@@ -0,0 +1,426 @@
1
+ /**
2
+ * SitePong SDK — Expo config plugin (the ONE plugin).
3
+ *
4
+ * `sitepong` is a single package that ships all native capabilities. Its config
5
+ * plugin therefore does what the old per-feature plugins used to do:
6
+ *
7
+ * 1. Screen recording / Watchtower tap capture — NO native config needed.
8
+ * (View-hierarchy capture; no screen-record entitlement, no purpose string.)
9
+ * The Watchtower engine is compiled into the Sitepong pod, so there is no
10
+ * Podfile patch anymore — that whole fragility is gone.
11
+ * 2. Device ID — writes PrivacyInfo.xcprivacy declaring Keychain-backed device
12
+ * id usage for analytics (Apple requirement since May 2024). Always applied.
13
+ * 3. Live Activities — a WidgetKit app-extension target. This is the ONLY piece
14
+ * that alters the Xcode project, so it is OPT-IN: enable with
15
+ * ["sitepong", { "liveActivities": true }]
16
+ * (optionally { "appGroupId": "group.com.myapp" }). A plain ["sitepong"]
17
+ * links capture + device id and leaves the Xcode project otherwise untouched.
18
+ *
19
+ * Usage:
20
+ * ["sitepong"] // capture + device id
21
+ * ["sitepong", { "liveActivities": true }] // + Live Activities
22
+ * ["sitepong", { "liveActivities": true, "appGroupId": "group.com.myapp" }]
23
+ */
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ // Resolve Expo's config-plugins from the CONSUMING app (this package is often
28
+ // symlinked and has no expo of its own). expo prebuild runs in the app context.
29
+ function resolveConfigPlugins() {
30
+ const candidates = ['@expo/config-plugins', 'expo/config-plugins'];
31
+ const searchPaths = [
32
+ process.cwd(),
33
+ path.join(process.cwd(), 'node_modules'),
34
+ __dirname,
35
+ ];
36
+ for (const name of candidates) {
37
+ try {
38
+ return require(require.resolve(name, { paths: searchPaths }));
39
+ } catch (_) {
40
+ /* try next */
41
+ }
42
+ }
43
+ return require('@expo/config-plugins');
44
+ }
45
+
46
+ const {
47
+ withXcodeProject,
48
+ withInfoPlist,
49
+ withEntitlementsPlist,
50
+ } = resolveConfigPlugins();
51
+
52
+ // ─── Device ID: PrivacyInfo.xcprivacy ────────────────────────────────────────
53
+
54
+ const PRIVACY_INFO = `<?xml version="1.0" encoding="UTF-8"?>
55
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
56
+ <plist version="1.0">
57
+ <dict>
58
+ <key>NSPrivacyTracking</key>
59
+ <false/>
60
+ <key>NSPrivacyCollectedDataTypes</key>
61
+ <array>
62
+ <dict>
63
+ <key>NSPrivacyCollectedDataType</key>
64
+ <string>NSPrivacyCollectedDataTypeDeviceID</string>
65
+ <key>NSPrivacyCollectedDataTypeLinked</key>
66
+ <false/>
67
+ <key>NSPrivacyCollectedDataTypeTracking</key>
68
+ <false/>
69
+ <key>NSPrivacyCollectedDataTypePurposes</key>
70
+ <array>
71
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
72
+ </array>
73
+ </dict>
74
+ </array>
75
+ <key>NSPrivacyAccessedAPITypes</key>
76
+ <array>
77
+ <dict>
78
+ <key>NSPrivacyAccessedAPIType</key>
79
+ <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
80
+ <key>NSPrivacyAccessedAPITypeReasons</key>
81
+ <array>
82
+ <string>CA92.1</string>
83
+ </array>
84
+ </dict>
85
+ </array>
86
+ </dict>
87
+ </plist>`;
88
+
89
+ function withDeviceIdPrivacy(config) {
90
+ return withXcodeProject(config, async (cfg) => {
91
+ const projectRoot = cfg.modRequest.projectRoot;
92
+ const iosDir = path.join(
93
+ projectRoot,
94
+ 'ios',
95
+ cfg.modRequest.projectName || '',
96
+ );
97
+ const privacyPath = path.join(iosDir, 'PrivacyInfo.xcprivacy');
98
+ if (!fs.existsSync(privacyPath)) {
99
+ fs.mkdirSync(path.dirname(privacyPath), { recursive: true });
100
+ fs.writeFileSync(privacyPath, PRIVACY_INFO);
101
+ }
102
+ return cfg;
103
+ });
104
+ }
105
+
106
+ // ─── Live Activities: WidgetKit app-extension target (opt-in) ────────────────
107
+
108
+ const EXTENSION_NAME = 'SitePongLiveActivityWidget';
109
+
110
+ // All Swift compiled into the Widget Extension target. These live under
111
+ // ios/LiveActivity/ in the sitepong package (they are excluded from the main
112
+ // pod by Sitepong.podspec). SitePongActivityAttributes is shared with the main
113
+ // app module and is also compiled into the pod — the extension needs its own
114
+ // copy because app extensions don't link the app's pods.
115
+ const WIDGET_SOURCE_FILES = [
116
+ { src: 'ios/LiveActivity/SitePongActivityAttributes.swift', name: 'SitePongActivityAttributes.swift' },
117
+ { src: 'ios/LiveActivity/widget/SitePongWidgetBundle.swift', name: 'SitePongWidgetBundle.swift' },
118
+ { src: 'ios/LiveActivity/widget/SitePongLiveActivityWidget.swift', name: 'SitePongLiveActivityWidget.swift' },
119
+ { src: 'ios/LiveActivity/widget/DSLRenderer.swift', name: 'DSLRenderer.swift' },
120
+ // DSLNode + ConfigStore are shared with the main app module (compiled into the
121
+ // Sitepong pod), so they live at ios/LiveActivity/ root, not under widget/.
122
+ { src: 'ios/LiveActivity/DSLNode.swift', name: 'DSLNode.swift' },
123
+ { src: 'ios/LiveActivity/widget/DSLAnimations.swift', name: 'DSLAnimations.swift' },
124
+ { src: 'ios/LiveActivity/ConfigStore.swift', name: 'ConfigStore.swift' },
125
+ { src: 'ios/LiveActivity/widget/ColorHex.swift', name: 'ColorHex.swift' },
126
+ ];
127
+
128
+ const WIDGET_INFO_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
129
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
130
+ <plist version="1.0">
131
+ <dict>
132
+ <key>NSExtension</key>
133
+ <dict>
134
+ <key>NSExtensionPointIdentifier</key>
135
+ <string>com.apple.widgetkit-extension</string>
136
+ </dict>
137
+ <key>CFBundleExecutable</key>
138
+ <string>$(EXECUTABLE_NAME)</string>
139
+ <key>CFBundleIdentifier</key>
140
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
141
+ <key>CFBundleName</key>
142
+ <string>$(PRODUCT_NAME)</string>
143
+ <key>CFBundleDisplayName</key>
144
+ <string>${EXTENSION_NAME}</string>
145
+ <key>CFBundlePackageType</key>
146
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
147
+ <key>CFBundleShortVersionString</key>
148
+ <string>$(MARKETING_VERSION)</string>
149
+ <key>CFBundleVersion</key>
150
+ <string>$(CURRENT_PROJECT_VERSION)</string>
151
+ </dict>
152
+ </plist>`;
153
+
154
+ function generateEntitlementsPlist(entitlements) {
155
+ let entries = '';
156
+ for (const [key, value] of Object.entries(entitlements)) {
157
+ if (Array.isArray(value)) {
158
+ entries += ` <key>${key}</key>\n <array>\n`;
159
+ for (const item of value) entries += ` <string>${item}</string>\n`;
160
+ entries += ` </array>\n`;
161
+ } else if (typeof value === 'string') {
162
+ entries += ` <key>${key}</key>\n <string>${value}</string>\n`;
163
+ } else if (typeof value === 'boolean') {
164
+ entries += ` <key>${key}</key>\n <${value}/>\n`;
165
+ }
166
+ }
167
+ return `<?xml version="1.0" encoding="UTF-8"?>
168
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
169
+ <plist version="1.0">
170
+ <dict>
171
+ ${entries}</dict>
172
+ </plist>`;
173
+ }
174
+
175
+ function withLiveActivities(config, props) {
176
+ // 1. aps-environment entitlement on the main app (push tokens for updates).
177
+ config = withEntitlementsPlist(config, (cfg) => {
178
+ cfg.modResults['aps-environment'] =
179
+ cfg.modResults['aps-environment'] || 'development';
180
+ return cfg;
181
+ });
182
+
183
+ // 2. Live Activities support in the main Info.plist.
184
+ config = withInfoPlist(config, (cfg) => {
185
+ cfg.modResults.NSSupportsLiveActivities = true;
186
+ cfg.modResults.NSSupportsLiveActivitiesFrequentUpdates = true;
187
+ return cfg;
188
+ });
189
+
190
+ // 3. Add the Widget Extension target to the Xcode project.
191
+ config = withXcodeProject(config, async (cfg) => {
192
+ const xcodeProject = cfg.modResults;
193
+ const projectRoot = cfg.modRequest.projectRoot;
194
+ const bundleId = cfg.ios?.bundleIdentifier || 'com.app';
195
+ const extensionBundleId = `${bundleId}.${EXTENSION_NAME}`;
196
+
197
+ const extensionDir = path.join(projectRoot, 'ios', EXTENSION_NAME);
198
+ const packageRoot = __dirname;
199
+
200
+ if (!fs.existsSync(extensionDir)) {
201
+ fs.mkdirSync(extensionDir, { recursive: true });
202
+ }
203
+
204
+ for (const file of WIDGET_SOURCE_FILES) {
205
+ const srcPath = path.join(packageRoot, file.src);
206
+ const dstPath = path.join(extensionDir, file.name);
207
+ if (!fs.existsSync(srcPath)) {
208
+ console.warn(`[sitepong] Missing Live Activity source: ${srcPath}`);
209
+ continue;
210
+ }
211
+ fs.copyFileSync(srcPath, dstPath);
212
+ }
213
+
214
+ fs.writeFileSync(path.join(extensionDir, 'Info.plist'), WIDGET_INFO_PLIST);
215
+
216
+ const entitlements = {};
217
+ if (props.appGroupId) {
218
+ entitlements['com.apple.security.application-groups'] = [props.appGroupId];
219
+ }
220
+ fs.writeFileSync(
221
+ path.join(extensionDir, `${EXTENSION_NAME}.entitlements`),
222
+ generateEntitlementsPlist(entitlements),
223
+ );
224
+
225
+ if (xcodeProject.pbxTargetByName(EXTENSION_NAME)) {
226
+ return cfg; // idempotent
227
+ }
228
+
229
+ const target = xcodeProject.addTarget(
230
+ EXTENSION_NAME,
231
+ 'app_extension',
232
+ EXTENSION_NAME,
233
+ extensionBundleId,
234
+ );
235
+ if (!target) {
236
+ console.warn('[sitepong] Failed to add Live Activity extension target');
237
+ return cfg;
238
+ }
239
+
240
+ const pbxFileRef = xcodeProject.hash.project.objects['PBXFileReference'];
241
+ const sourceFileRefs = [];
242
+ for (const file of WIDGET_SOURCE_FILES) {
243
+ const ref = xcodeProject.generateUuid();
244
+ pbxFileRef[ref] = {
245
+ isa: 'PBXFileReference',
246
+ lastKnownFileType: 'sourcecode.swift',
247
+ name: file.name,
248
+ path: file.name,
249
+ sourceTree: '"<group>"',
250
+ };
251
+ pbxFileRef[ref + '_comment'] = file.name;
252
+ sourceFileRefs.push({ ref, name: file.name });
253
+ }
254
+
255
+ const plistRef = xcodeProject.generateUuid();
256
+ pbxFileRef[plistRef] = {
257
+ isa: 'PBXFileReference',
258
+ lastKnownFileType: 'text.plist.xml',
259
+ name: 'Info.plist',
260
+ path: 'Info.plist',
261
+ sourceTree: '"<group>"',
262
+ };
263
+ pbxFileRef[plistRef + '_comment'] = 'Info.plist';
264
+
265
+ const entRef = xcodeProject.generateUuid();
266
+ pbxFileRef[entRef] = {
267
+ isa: 'PBXFileReference',
268
+ lastKnownFileType: 'text.plist.entitlements',
269
+ name: `${EXTENSION_NAME}.entitlements`,
270
+ path: `${EXTENSION_NAME}.entitlements`,
271
+ sourceTree: '"<group>"',
272
+ };
273
+ pbxFileRef[entRef + '_comment'] = `${EXTENSION_NAME}.entitlements`;
274
+
275
+ const groupUuid = xcodeProject.generateUuid();
276
+ const pbxGroup = xcodeProject.hash.project.objects['PBXGroup'];
277
+ pbxGroup[groupUuid] = {
278
+ isa: 'PBXGroup',
279
+ children: [
280
+ ...sourceFileRefs.map((f) => ({ value: f.ref, comment: f.name })),
281
+ { value: plistRef, comment: 'Info.plist' },
282
+ { value: entRef, comment: `${EXTENSION_NAME}.entitlements` },
283
+ ],
284
+ name: EXTENSION_NAME,
285
+ path: EXTENSION_NAME,
286
+ sourceTree: '"<group>"',
287
+ };
288
+ pbxGroup[groupUuid + '_comment'] = EXTENSION_NAME;
289
+
290
+ const mainGroup = xcodeProject.getFirstProject().firstProject.mainGroup;
291
+ const mainGroupObj = pbxGroup[mainGroup];
292
+ if (mainGroupObj && mainGroupObj.children) {
293
+ mainGroupObj.children.push({ value: groupUuid, comment: EXTENSION_NAME });
294
+ }
295
+
296
+ const pbxBuildFile = xcodeProject.hash.project.objects['PBXBuildFile'];
297
+ const buildFileEntries = sourceFileRefs.map((f) => {
298
+ const buildUuid = xcodeProject.generateUuid();
299
+ pbxBuildFile[buildUuid] = {
300
+ isa: 'PBXBuildFile',
301
+ fileRef: f.ref,
302
+ fileRef_comment: f.name,
303
+ };
304
+ pbxBuildFile[buildUuid + '_comment'] = `${f.name} in Sources`;
305
+ return { value: buildUuid, comment: `${f.name} in Sources` };
306
+ });
307
+
308
+ if (!xcodeProject.hash.project.objects['PBXSourcesBuildPhase']) {
309
+ xcodeProject.hash.project.objects['PBXSourcesBuildPhase'] = {};
310
+ }
311
+ const sourcesPhaseUuid = xcodeProject.generateUuid();
312
+ xcodeProject.hash.project.objects['PBXSourcesBuildPhase'][sourcesPhaseUuid] = {
313
+ isa: 'PBXSourcesBuildPhase',
314
+ buildActionMask: 2147483647,
315
+ files: buildFileEntries,
316
+ runOnlyForDeploymentPostprocessing: 0,
317
+ };
318
+ xcodeProject.hash.project.objects['PBXSourcesBuildPhase'][sourcesPhaseUuid + '_comment'] = 'Sources';
319
+
320
+ if (!xcodeProject.hash.project.objects['PBXFrameworksBuildPhase']) {
321
+ xcodeProject.hash.project.objects['PBXFrameworksBuildPhase'] = {};
322
+ }
323
+ const frameworksPhaseUuid = xcodeProject.generateUuid();
324
+ xcodeProject.hash.project.objects['PBXFrameworksBuildPhase'][frameworksPhaseUuid] = {
325
+ isa: 'PBXFrameworksBuildPhase',
326
+ buildActionMask: 2147483647,
327
+ files: [],
328
+ runOnlyForDeploymentPostprocessing: 0,
329
+ };
330
+ xcodeProject.hash.project.objects['PBXFrameworksBuildPhase'][frameworksPhaseUuid + '_comment'] = 'Frameworks';
331
+
332
+ if (!xcodeProject.hash.project.objects['PBXResourcesBuildPhase']) {
333
+ xcodeProject.hash.project.objects['PBXResourcesBuildPhase'] = {};
334
+ }
335
+ const resourcesPhaseUuid = xcodeProject.generateUuid();
336
+ xcodeProject.hash.project.objects['PBXResourcesBuildPhase'][resourcesPhaseUuid] = {
337
+ isa: 'PBXResourcesBuildPhase',
338
+ buildActionMask: 2147483647,
339
+ files: [],
340
+ runOnlyForDeploymentPostprocessing: 0,
341
+ };
342
+ xcodeProject.hash.project.objects['PBXResourcesBuildPhase'][resourcesPhaseUuid + '_comment'] = 'Resources';
343
+
344
+ const targetUuid = target.uuid;
345
+ const nativeTargets = xcodeProject.hash.project.objects['PBXNativeTarget'];
346
+ if (nativeTargets[targetUuid]) {
347
+ nativeTargets[targetUuid].buildPhases = [
348
+ { value: sourcesPhaseUuid, comment: 'Sources' },
349
+ { value: frameworksPhaseUuid, comment: 'Frameworks' },
350
+ { value: resourcesPhaseUuid, comment: 'Resources' },
351
+ ];
352
+ }
353
+
354
+ const configurations = xcodeProject.pbxXCBuildConfigurationSection();
355
+ for (const key in configurations) {
356
+ const c = configurations[key];
357
+ if (c && typeof c === 'object' && c.name && c.buildSettings) {
358
+ if (
359
+ c.buildSettings.PRODUCT_NAME === `"${EXTENSION_NAME}"` ||
360
+ c.buildSettings.PRODUCT_NAME === EXTENSION_NAME
361
+ ) {
362
+ c.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = '16.2';
363
+ c.buildSettings.SWIFT_VERSION = '5.0';
364
+ c.buildSettings.CODE_SIGN_ENTITLEMENTS = `${EXTENSION_NAME}/${EXTENSION_NAME}.entitlements`;
365
+ c.buildSettings.INFOPLIST_FILE = `${EXTENSION_NAME}/Info.plist`;
366
+ c.buildSettings.PRODUCT_BUNDLE_IDENTIFIER = extensionBundleId;
367
+ c.buildSettings.GENERATE_INFOPLIST_FILE = 'NO';
368
+ c.buildSettings.CURRENT_PROJECT_VERSION = '1';
369
+ c.buildSettings.MARKETING_VERSION = '1.0';
370
+ c.buildSettings.CODE_SIGN_STYLE = 'Manual';
371
+ c.buildSettings.CODE_SIGN_IDENTITY = '"iPhone Distribution"';
372
+ c.buildSettings.SKIP_INSTALL = 'YES';
373
+ c.buildSettings.LD_RUNPATH_SEARCH_PATHS =
374
+ '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"';
375
+
376
+ for (const mk in configurations) {
377
+ const mc = configurations[mk];
378
+ if (
379
+ mc?.buildSettings?.DEVELOPMENT_TEAM &&
380
+ mc.name === c.name &&
381
+ mc.buildSettings.PRODUCT_BUNDLE_IDENTIFIER !== extensionBundleId
382
+ ) {
383
+ c.buildSettings.DEVELOPMENT_TEAM = mc.buildSettings.DEVELOPMENT_TEAM;
384
+ break;
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ const mainTarget = xcodeProject.getFirstTarget();
392
+ if (mainTarget && mainTarget.firstTarget) {
393
+ xcodeProject.addTargetDependency(mainTarget.firstTarget.uuid, [target.uuid]);
394
+ const copyPhase = xcodeProject.addBuildPhase(
395
+ [],
396
+ 'PBXCopyFilesBuildPhase',
397
+ 'Embed App Extensions',
398
+ mainTarget.firstTarget.uuid,
399
+ 'app_extension',
400
+ );
401
+ if (copyPhase) {
402
+ copyPhase.buildPhase.dstSubfolderSpec = 13;
403
+ }
404
+ }
405
+
406
+ return cfg;
407
+ });
408
+
409
+ return config;
410
+ }
411
+
412
+ // ─── Entry ───────────────────────────────────────────────────────────────────
413
+
414
+ function withSitePong(config, props = {}) {
415
+ // Device id privacy manifest is always safe to apply.
416
+ config = withDeviceIdPrivacy(config);
417
+
418
+ // Live Activities alter the Xcode project — opt in explicitly.
419
+ if (props && (props.liveActivities === true || props.appGroupId)) {
420
+ config = withLiveActivities(config, props);
421
+ }
422
+
423
+ return config;
424
+ }
425
+
426
+ module.exports = withSitePong;