@bacons/apple-targets 0.1.17 → 0.1.19
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/README.md +84 -31
- package/build/config-plugin.js +1 -1
- package/build/config.d.ts +1 -13
- package/build/icon/withImageAsset.js +6 -10
- package/build/icon/withIosIcon.js +5 -9
- package/build/withWidget.js +62 -33
- package/build/withXcodeChanges.d.ts +3 -0
- package/build/withXcodeChanges.js +44 -9
- package/package.json +5 -3
- package/build/withLinkAppIntent.d.ts +0 -7
- package/build/withLinkAppIntent.js +0 -86
package/README.md
CHANGED
|
@@ -178,8 +178,6 @@ The name of the target must match the name of the target directory.
|
|
|
178
178
|
|
|
179
179
|
Some files are required to be linked to both your target and the main target. To support this, you can add a top-level `_shared` directory. Any file in this directory will be linked to both the main target and the sub-target. You'll need to re-run prebuild every time you add, rename, or remove a file in this directory.
|
|
180
180
|
|
|
181
|
-
This is especially useful for Live Activities and Intents.
|
|
182
|
-
|
|
183
181
|
## `exportJs`
|
|
184
182
|
|
|
185
183
|
The `exportJs` option should be used when the target uses React Native (App Clip, Share extension). It works by linking the main target's `Bundle React Native code and images` build phase to the target. This will ensure that production builds (`Release`) bundle the main JS entry file with Metro, and embed the bundle/assets for offline use.
|
|
@@ -319,6 +317,7 @@ If you experience issues building widgets, it might be because React Native is s
|
|
|
319
317
|
|
|
320
318
|
Some workarounds:
|
|
321
319
|
|
|
320
|
+
- Clear the SwiftUI previews cache: `xcrun simctl --set previews delete all`
|
|
322
321
|
- Prebuild without React Native: `npx expo prebuild --template node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean`
|
|
323
322
|
- If the widget doesn't show on the home screen when building the app, use iOS 18. You can long press the app icon and select the widget display options to transform the app icon into the widget.
|
|
324
323
|
|
|
@@ -401,47 +400,101 @@ let defaults = UserDefaults(suiteName:
|
|
|
401
400
|
let index = defaults?.string(forKey: "myKey")
|
|
402
401
|
```
|
|
403
402
|
|
|
403
|
+
### More data updates
|
|
404
|
+
|
|
405
|
+
For more advanced uses, I recommend the following resources:
|
|
406
|
+
|
|
407
|
+
- Updating widgets when the app is in the background: [Keeping A Widget Up-to-Date](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date).
|
|
408
|
+
|
|
404
409
|
## Xcode parsing
|
|
405
410
|
|
|
406
411
|
This plugin makes use of my proprietary Xcode parsing library, [`@bacons/xcode`](https://github.com/evanbacon/xcode). It's mostly typed, very untested, and possibly full of bugs––however, it's still 10x nicer than the alternative.
|
|
407
412
|
|
|
408
|
-
##
|
|
413
|
+
## App Clips
|
|
409
414
|
|
|
410
|
-
|
|
415
|
+
App Clips leverage the true power of Expo Router, enabling you to link a website and native app to just instantly open the native app on iOS. They're pretty hard to get working though.
|
|
411
416
|
|
|
412
|
-
|
|
417
|
+
Here are a few notes from my experience building https://pillarvalley.expo.app (open on iOS to test).
|
|
413
418
|
|
|
414
|
-
|
|
419
|
+
Build the app first, then the website. You can always instantly update the website if it's wrong. This includes the AASA, and metadata.
|
|
420
|
+
|
|
421
|
+
You may need [this RN patch](https://github.com/facebook/react-native/pull/47000) to get your project working, otherwise it'll crash when launched from Test Flight. Alternatively, you can add App Clip experiences in App Store Connect and it'll launch as expected.
|
|
422
|
+
|
|
423
|
+
After running prebuild, open the project in Xcode and navigate to the signing tab for each target, this'll ensure the first version of codesigning is absolutely correct. We'll need to adjust EAS Build to ensure it can do this too.
|
|
424
|
+
|
|
425
|
+
Ensure your App Clip does not have `expo-updates` installed, otherwise it'll fail to build with some cryptic error about React missing in the AppDelegate.
|
|
426
|
+
|
|
427
|
+
Ensure all the build numbers are the same across the `CURRENT_PROJECT_VERSION` and `CFBundleVersion` (Info.plist) otherwise the app will fail to build.
|
|
428
|
+
|
|
429
|
+
Ensure you add a `public/.well-known/apple-app-site-association` file to your website and deploy it to the web (`eas deploy --prod`). Here's [an example](https://github.com/EvanBacon/pillar-valley/blob/d5ab82ae04f519310acf4b31aad8d9e22eb3747d/public/.well-known/apple-app-site-association#L27-L29).
|
|
430
|
+
|
|
431
|
+
The value will be `<Apple Team ID>.<App Clip Bundle ID>`:
|
|
432
|
+
|
|
433
|
+
```
|
|
415
434
|
{
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
label: "Bacon",
|
|
420
|
-
// Name of an SF Symbol (no custom icons are supported).
|
|
421
|
-
icon: "laurel.leading",
|
|
422
|
-
// Universal link to open.
|
|
423
|
-
url: "https://pillarvalley.netlify.app/settings",
|
|
424
|
-
// Auxiliary data
|
|
425
|
-
displayName: "Open Bacon AI",
|
|
426
|
-
description: "Launch the Bacon AI app.",
|
|
427
|
-
},
|
|
428
|
-
];
|
|
435
|
+
"appclips": {
|
|
436
|
+
"apps": ["QQ57RJ5UTD.com.evanbacon.pillarvalley.clip"]
|
|
437
|
+
}
|
|
429
438
|
}
|
|
430
439
|
```
|
|
431
440
|
|
|
432
|
-
|
|
441
|
+
Add the website URL to your App Clip entitlements (not the main entitlements). Here's an example with `https://pillarvalley.expo.app`:
|
|
433
442
|
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// widgetControl0()
|
|
440
|
-
// widgetControl1()
|
|
441
|
-
// }
|
|
442
|
-
// }
|
|
443
|
+
```xml
|
|
444
|
+
<key>com.apple.developer.associated-domains</key>
|
|
445
|
+
<array>
|
|
446
|
+
<string>appclips:pillarvalley.expo.app</string>
|
|
447
|
+
</array>
|
|
443
448
|
```
|
|
444
449
|
|
|
445
|
-
|
|
450
|
+
If this isn't done, then your App Clip will only be able to be launched from the default App Store URL: `https://appclip.apple.com/id?p=com.evanbacon.pillarvalley.clip` (where your App Clip bundle ID will be the ID in the URL).
|
|
451
|
+
|
|
452
|
+
You should handle redirection from this default URL too with a [`app/+native-intent.ts`](https://docs.expo.dev/router/advanced/native-intent/) file:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
export function redirectSystemPath({ path }: { path: string }): string {
|
|
456
|
+
try {
|
|
457
|
+
// Handle App Clip default page redirection.
|
|
458
|
+
// If path matches https://appclip.apple.com/id?p=com.evanbacon.pillarvalley.clip (with any query parameters), then redirect to `/` path.
|
|
459
|
+
const url = new URL(path);
|
|
460
|
+
if (url.hostname === "appclip.apple.com") {
|
|
461
|
+
// Redirect to the root path and make the original URL available as a query parameter (optional).
|
|
462
|
+
return "/?ref=" + encodeURIComponent(path);
|
|
463
|
+
}
|
|
464
|
+
return path;
|
|
465
|
+
} catch {
|
|
466
|
+
return path;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
You should use `expo-linking` to get URLs related to the App Clip as the upstream React Native Linking has some issues handling App Clips.
|
|
472
|
+
|
|
473
|
+
When you publish an App Clip, the binary will take about 5 minutes to show up in the App Store (after it's approved) but the App Clip will take more like 25 minutes to show up in your website.
|
|
474
|
+
|
|
475
|
+
You also need to add some meta tags to your website. These need to run fast so I recommend putting them in your `app/+html.tsx` file:
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
<meta
|
|
479
|
+
name="apple-itunes-app"
|
|
480
|
+
content={
|
|
481
|
+
"app-id=1336398804, app-clip-bundle-id=com.evanbacon.pillarvalley.clip, app-clip-display=card"
|
|
482
|
+
}
|
|
483
|
+
/>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
You should also add the `og:image` property using `expo-router/head`. [Learn more](https://developer.apple.com/documentation/appclip/supporting-invocations-from-your-website-and-the-messages-app). It seems like an absolute path to a png image that is `1200×630` in dimensions ([based on this](https://developer.apple.com/library/archive/technotes/tn2444/_index.html)).
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
<Head>
|
|
490
|
+
{/* Required for app clips: */}
|
|
491
|
+
{/* https://developer.apple.com/documentation/appclip/supporting-invocations-from-your-website-and-the-messages-app */}
|
|
492
|
+
<meta property="og:image" content="https://pillarvalley.expo.app/og.png" />
|
|
493
|
+
</Head>
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
You also need a `1800x1200` image for the App Store Connect image preview, so make both of these images around the same time.
|
|
497
|
+
|
|
498
|
+
Launch App Clips from Test Flight to test deep linking. It doesn't seem like there's any reasonable way to test launching from your website in development. I got this to work once by setting up a local experience in my app's "Settings > Developer" screen, then installing the app, opening the website, deleting the app, then installing the App Clip without the app. You'll mostly need to go with God on this one.
|
|
446
499
|
|
|
447
|
-
|
|
500
|
+
You can generate codes using the CLI tool [download here](https://developer.apple.com/download/all/?q=%22app%20clip%22).
|
package/build/config-plugin.js
CHANGED
|
@@ -20,7 +20,7 @@ const withTargetsDir = (config, _props) => {
|
|
|
20
20
|
hasWarned = true;
|
|
21
21
|
console.warn((0, chalk_1.default) `{yellow [bacons/apple-targets]} Expo config is missing required {cyan ios.appleTeamId} property. Find this in Xcode and add to the Expo Config to correct. iOS builds may fail until this is corrected.`);
|
|
22
22
|
}
|
|
23
|
-
const targets = (0, glob_1.
|
|
23
|
+
const targets = (0, glob_1.sync)(`${root}/${match}/expo-target.config.@(json|js)`, {
|
|
24
24
|
// const targets = globSync(`./targets/action/expo-target.config.@(json|js)`, {
|
|
25
25
|
cwd: projectRoot,
|
|
26
26
|
absolute: true,
|
package/build/config.d.ts
CHANGED
|
@@ -3,17 +3,6 @@ export type DynamicColor = {
|
|
|
3
3
|
light: string;
|
|
4
4
|
dark?: string;
|
|
5
5
|
};
|
|
6
|
-
export type AppIntent = {
|
|
7
|
-
/** Main text to show in the control */
|
|
8
|
-
label: string;
|
|
9
|
-
/** SF Symbol name */
|
|
10
|
-
icon: string;
|
|
11
|
-
displayName: string;
|
|
12
|
-
description: string;
|
|
13
|
-
kind?: string;
|
|
14
|
-
/** URL to open when the intent is triggered. Should be a universal link. */
|
|
15
|
-
url: string;
|
|
16
|
-
};
|
|
17
6
|
export type Entitlements = Partial<{
|
|
18
7
|
"com.apple.developer.healthkit": boolean;
|
|
19
8
|
"com.apple.developer.healthkit.access": string[];
|
|
@@ -70,6 +59,7 @@ export type Entitlements = Partial<{
|
|
|
70
59
|
"com.apple.developer.driverkit.transport.hid": boolean;
|
|
71
60
|
"com.apple.developer.driverkit.family.audio": boolean;
|
|
72
61
|
"com.apple.developer.shared-with-you": boolean;
|
|
62
|
+
"com.apple.developer.associated-domains": string[];
|
|
73
63
|
}>;
|
|
74
64
|
export type Config = {
|
|
75
65
|
/**
|
|
@@ -119,7 +109,5 @@ export type Config = {
|
|
|
119
109
|
}>;
|
|
120
110
|
/** Should the release build export the JS bundle and embed. Intended for App Clips and Share Extensions where you may want to use React Native. */
|
|
121
111
|
exportJs?: boolean;
|
|
122
|
-
/** App Intents to generate. */
|
|
123
|
-
intents?: AppIntent[];
|
|
124
112
|
};
|
|
125
113
|
export type ConfigFunction = (config: import("expo/config").ExpoConfig) => Config;
|
|
@@ -27,7 +27,7 @@ exports.generateWatchIconsInternalAsync = exports.generateIconsInternalAsync = e
|
|
|
27
27
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
28
28
|
const image_utils_1 = require("@expo/image-utils");
|
|
29
29
|
const AssetContents_1 = require("@expo/prebuild-config/build/plugins/icons/AssetContents");
|
|
30
|
-
const fs = __importStar(require("fs"));
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
31
|
const path_1 = __importStar(require("path"));
|
|
32
32
|
const withImageAsset = (config, { cwd, name, image }) => {
|
|
33
33
|
return (0, config_plugins_1.withDangerousMod)(config, [
|
|
@@ -37,9 +37,7 @@ const withImageAsset = (config, { cwd, name, image }) => {
|
|
|
37
37
|
const iosNamedProjectRoot = (0, path_1.join)(projectRoot, cwd);
|
|
38
38
|
const imgPath = `Assets.xcassets/${name}.imageset`;
|
|
39
39
|
// Ensure the Images.xcassets/AppIcon.appiconset path exists
|
|
40
|
-
await fs.
|
|
41
|
-
recursive: true,
|
|
42
|
-
});
|
|
40
|
+
await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, imgPath));
|
|
43
41
|
const userDefinedIcon = typeof image === "string"
|
|
44
42
|
? { "1x": image, "2x": undefined, "3x": undefined }
|
|
45
43
|
: image;
|
|
@@ -122,9 +120,7 @@ exports.ICON_CONTENTS = [
|
|
|
122
120
|
];
|
|
123
121
|
async function setIconsAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent) {
|
|
124
122
|
// Ensure the Images.xcassets/AppIcon.appiconset path exists
|
|
125
|
-
await fs.
|
|
126
|
-
recursive: true,
|
|
127
|
-
});
|
|
123
|
+
await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH));
|
|
128
124
|
// Finally, write the Config.json
|
|
129
125
|
await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
|
|
130
126
|
images: await generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent),
|
|
@@ -158,7 +154,7 @@ async function generateResizedImageAsync(icon, name, projectRoot, iosNamedProjec
|
|
|
158
154
|
});
|
|
159
155
|
// Write image buffer to the file system.
|
|
160
156
|
const assetPath = (0, path_1.join)(iosNamedProjectRoot, `Assets.xcassets/${name}.imageset`, filename);
|
|
161
|
-
await fs.
|
|
157
|
+
await fs.writeFile(assetPath, source);
|
|
162
158
|
if (filename) {
|
|
163
159
|
imgEntry.filename = filename;
|
|
164
160
|
}
|
|
@@ -200,7 +196,7 @@ async function generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot
|
|
|
200
196
|
});
|
|
201
197
|
// Write image buffer to the file system.
|
|
202
198
|
const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
|
|
203
|
-
await fs.
|
|
199
|
+
await fs.writeFile(assetPath, source);
|
|
204
200
|
// Save a reference to the generated image so we don't create a duplicate.
|
|
205
201
|
generatedIcons[filename] = true;
|
|
206
202
|
}
|
|
@@ -238,7 +234,7 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
|
|
|
238
234
|
});
|
|
239
235
|
// Write image buffer to the file system.
|
|
240
236
|
const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
|
|
241
|
-
await fs.
|
|
237
|
+
await fs.writeFile(assetPath, source);
|
|
242
238
|
imagesJson.push({
|
|
243
239
|
filename: getAppleIconName(size, 1),
|
|
244
240
|
idiom: "universal",
|
|
@@ -27,7 +27,7 @@ exports.generateWatchIconsInternalAsync = exports.generateIconsInternalAsync = e
|
|
|
27
27
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
28
28
|
const image_utils_1 = require("@expo/image-utils");
|
|
29
29
|
const AssetContents_1 = require("@expo/prebuild-config/build/plugins/icons/AssetContents");
|
|
30
|
-
const fs = __importStar(require("fs"));
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
31
|
const path_1 = require("path");
|
|
32
32
|
// TODO: support dark, tinted, and universal icons for widgets.
|
|
33
33
|
const withIosIcon = (config, { cwd, type, iconFilePath, isTransparent = false }) => {
|
|
@@ -38,9 +38,7 @@ const withIosIcon = (config, { cwd, type, iconFilePath, isTransparent = false })
|
|
|
38
38
|
const namedProjectRoot = (0, path_1.join)(projectRoot, cwd);
|
|
39
39
|
if (type === "watch") {
|
|
40
40
|
// Ensure the Images.xcassets/AppIcon.appiconset path exists
|
|
41
|
-
await fs.
|
|
42
|
-
recursive: true,
|
|
43
|
-
});
|
|
41
|
+
await fs.ensureDir((0, path_1.join)(namedProjectRoot, IMAGESET_PATH));
|
|
44
42
|
// Finally, write the Config.json
|
|
45
43
|
await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(namedProjectRoot, IMAGESET_PATH), {
|
|
46
44
|
images: await generateWatchIconsInternalAsync(iconFilePath, projectRoot, namedProjectRoot, cwd, isTransparent),
|
|
@@ -121,9 +119,7 @@ exports.ICON_CONTENTS = [
|
|
|
121
119
|
];
|
|
122
120
|
async function setIconsAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent, isTransparent) {
|
|
123
121
|
// Ensure the Images.xcassets/AppIcon.appiconset path exists
|
|
124
|
-
await fs.
|
|
125
|
-
recursive: true,
|
|
126
|
-
});
|
|
122
|
+
await fs.ensureDir((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH));
|
|
127
123
|
// Finally, write the Config.json
|
|
128
124
|
await (0, AssetContents_1.writeContentsJsonAsync)((0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH), {
|
|
129
125
|
images: await generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot, cacheComponent, isTransparent),
|
|
@@ -162,7 +158,7 @@ async function generateIconsInternalAsync(icon, projectRoot, iosNamedProjectRoot
|
|
|
162
158
|
});
|
|
163
159
|
// Write image buffer to the file system.
|
|
164
160
|
const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
|
|
165
|
-
await fs.
|
|
161
|
+
await fs.writeFile(assetPath, source);
|
|
166
162
|
// Save a reference to the generated image so we don't create a duplicate.
|
|
167
163
|
generatedIcons[filename] = true;
|
|
168
164
|
}
|
|
@@ -200,7 +196,7 @@ async function generateWatchIconsInternalAsync(icon, projectRoot, iosNamedProjec
|
|
|
200
196
|
});
|
|
201
197
|
// Write image buffer to the file system.
|
|
202
198
|
const assetPath = (0, path_1.join)(iosNamedProjectRoot, IMAGESET_PATH, filename);
|
|
203
|
-
await fs.
|
|
199
|
+
await fs.writeFile(assetPath, source);
|
|
204
200
|
imagesJson.push({
|
|
205
201
|
filename: getAppleIconName(size, 1),
|
|
206
202
|
idiom: "universal",
|
package/build/withWidget.js
CHANGED
|
@@ -15,7 +15,6 @@ const withIosIcon_1 = require("./icon/withIosIcon");
|
|
|
15
15
|
const target_1 = require("./target");
|
|
16
16
|
const withEasCredentials_1 = require("./withEasCredentials");
|
|
17
17
|
const withXcodeChanges_1 = require("./withXcodeChanges");
|
|
18
|
-
const withLinkAppIntent_1 = __importDefault(require("./withLinkAppIntent"));
|
|
19
18
|
const DEFAULT_DEPLOYMENT_TARGET = "18.0";
|
|
20
19
|
function memoize(fn) {
|
|
21
20
|
const cache = new Map();
|
|
@@ -52,7 +51,7 @@ function kebabToCamelCase(str) {
|
|
|
52
51
|
});
|
|
53
52
|
}
|
|
54
53
|
const withWidget = (config, props) => {
|
|
55
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
54
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
56
55
|
prebuildLogQueue.add(() => warnOnce((0, chalk_1.default) `\nUsing experimental Config Plugin {bold @bacons/apple-targets} that is subject to breaking changes.`));
|
|
57
56
|
// TODO: Magically based on the top-level folders in the `ios-widgets/` folder
|
|
58
57
|
if (props.icon && !/https?:\/\//.test(props.icon)) {
|
|
@@ -64,7 +63,7 @@ const withWidget = (config, props) => {
|
|
|
64
63
|
.replace(/^\/+/, "");
|
|
65
64
|
const widget = kebabToCamelCase(widgetDir);
|
|
66
65
|
const widgetFolderAbsolutePath = path_1.default.join((_b = (_a = config._internal) === null || _a === void 0 ? void 0 : _a.projectRoot) !== null && _b !== void 0 ? _b : "", props.directory);
|
|
67
|
-
const entitlementsFiles = (0, glob_1.
|
|
66
|
+
const entitlementsFiles = (0, glob_1.sync)("*.entitlements", {
|
|
68
67
|
absolute: true,
|
|
69
68
|
cwd: widgetFolderAbsolutePath,
|
|
70
69
|
});
|
|
@@ -75,11 +74,54 @@ const withWidget = (config, props) => {
|
|
|
75
74
|
if (entitlementsJson) {
|
|
76
75
|
// Apply default entitlements that must be present for a target to work.
|
|
77
76
|
const applyDefaultEntitlements = (entitlements) => {
|
|
78
|
-
var _a, _b;
|
|
77
|
+
var _a, _b, _c, _d, _e, _f;
|
|
79
78
|
if (props.type === "clip") {
|
|
80
79
|
entitlements["com.apple.developer.parent-application-identifiers"] = [
|
|
81
80
|
`$(AppIdentifierPrefix)${config.ios.bundleIdentifier}`,
|
|
82
81
|
];
|
|
82
|
+
// Try to extract the linked website from the original associated domains:
|
|
83
|
+
const associatedDomainsKey = "com.apple.developer.associated-domains";
|
|
84
|
+
// If the target doesn't explicitly define associated domains, then try to use the main app's associated domains.
|
|
85
|
+
if (!entitlements[associatedDomainsKey]) {
|
|
86
|
+
const associatedDomains = (_b = (_a = config.ios) === null || _a === void 0 ? void 0 : _a.associatedDomains) !== null && _b !== void 0 ? _b : (_d = (_c = config.ios) === null || _c === void 0 ? void 0 : _c.entitlements) === null || _d === void 0 ? void 0 : _d["com.apple.developer.associated-domains"];
|
|
87
|
+
if (!associatedDomains ||
|
|
88
|
+
!Array.isArray(associatedDomains) ||
|
|
89
|
+
associatedDomains.length === 0) {
|
|
90
|
+
warnOnce((0, chalk_1.default) `{yellow [${widget}]} Apple App Clip may require the associated domains entitlement but none were found in the Expo config.\nExample:\n${JSON.stringify({
|
|
91
|
+
ios: {
|
|
92
|
+
associatedDomains: [`applinks:placeholder.expo.app`],
|
|
93
|
+
},
|
|
94
|
+
}, null, 2)}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Associated domains are found:
|
|
98
|
+
// "applinks:pillarvalley.expo.app",
|
|
99
|
+
// "webcredentials:pillarvalley.expo.app",
|
|
100
|
+
// "activitycontinuation:pillarvalley.expo.app"
|
|
101
|
+
const sanitizedUrls = associatedDomains
|
|
102
|
+
.map((url) => {
|
|
103
|
+
return (url
|
|
104
|
+
.replace(/^(appclips|applinks|webcredentials|activitycontinuation):/, "")
|
|
105
|
+
// Remove trailing slashes
|
|
106
|
+
.replace(/\/$/, "")
|
|
107
|
+
// Remove http/https
|
|
108
|
+
.replace(/^https?:\/\//, ""));
|
|
109
|
+
})
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
const unique = [...new Set(sanitizedUrls)];
|
|
112
|
+
if (unique.length) {
|
|
113
|
+
warnOnce((0, chalk_1.default) `{gray [${widget}]} Apple App Clip expo-target.config.js missing associated domains entitlements in the target config. Using the following defaults:\n${JSON.stringify({
|
|
114
|
+
entitlements: {
|
|
115
|
+
[associatedDomainsKey]: [
|
|
116
|
+
`appclips:${unique[0] || "mywebsite.expo.app"}`,
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
}, null, 2)}`);
|
|
120
|
+
// Add anyways
|
|
121
|
+
entitlements[associatedDomainsKey] = unique.map((url) => `appclips:${url}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
83
125
|
// NOTE: This doesn't seem to be required anymore (Oct 12 2024):
|
|
84
126
|
// entitlements["com.apple.developer.on-demand-install-capable"] = true;
|
|
85
127
|
}
|
|
@@ -90,7 +132,7 @@ const withWidget = (config, props) => {
|
|
|
90
132
|
!hasDefinedAppGroupsManually &&
|
|
91
133
|
// And the target is part of a predefined list of types that benefit from app groups that match the main app...
|
|
92
134
|
target_1.SHOULD_USE_APP_GROUPS_BY_DEFAULT[props.type]) {
|
|
93
|
-
const mainAppGroups = (
|
|
135
|
+
const mainAppGroups = (_f = (_e = config.ios) === null || _e === void 0 ? void 0 : _e.entitlements) === null || _f === void 0 ? void 0 : _f[APP_GROUP_KEY];
|
|
94
136
|
if (Array.isArray(mainAppGroups) && mainAppGroups.length > 0) {
|
|
95
137
|
// Then set the target app groups to match the main app.
|
|
96
138
|
entitlements[APP_GROUP_KEY] = mainAppGroups;
|
|
@@ -117,29 +159,6 @@ const withWidget = (config, props) => {
|
|
|
117
159
|
};
|
|
118
160
|
entitlementsJson = applyDefaultEntitlements(entitlementsJson);
|
|
119
161
|
}
|
|
120
|
-
if (props.intents) {
|
|
121
|
-
// Add local images:
|
|
122
|
-
// TODO: Apple only supports custom SF Symbols for intents, so we'll need to create a system to generate these from an SVG or accept .SFSymbol.svg files.
|
|
123
|
-
// props.intents.forEach((intent, index) => {
|
|
124
|
-
// // Is local image?
|
|
125
|
-
// if (intent.icon.match(/^\./)) {
|
|
126
|
-
// props.images ??= {};
|
|
127
|
-
// const intentIconName = "generated_expo_intent_icon_" + index;
|
|
128
|
-
// props.images[intentIconName] = intent.icon;
|
|
129
|
-
// props.intents![index].icon = intentIconName;
|
|
130
|
-
// }
|
|
131
|
-
// });
|
|
132
|
-
props.intents.forEach((intent) => {
|
|
133
|
-
if (intent.icon.match(/^\./)) {
|
|
134
|
-
throw new Error("Local images are not supported for intents. Use an SF Symbol name. From: " +
|
|
135
|
-
intent.icon);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
(0, withLinkAppIntent_1.default)(config, {
|
|
139
|
-
intents: props.intents,
|
|
140
|
-
targetRoot: widgetFolderAbsolutePath,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
162
|
// If the user defined entitlements, then overwrite any existing entitlements file
|
|
144
163
|
if (entitlementsJson) {
|
|
145
164
|
(0, config_plugins_1.withDangerousMod)(config, [
|
|
@@ -201,22 +220,32 @@ const withWidget = (config, props) => {
|
|
|
201
220
|
const mainAppBundleId = config.ios.bundleIdentifier;
|
|
202
221
|
const bundleId = ((_d = props.bundleIdentifier) === null || _d === void 0 ? void 0 : _d.startsWith("."))
|
|
203
222
|
? mainAppBundleId + props.bundleIdentifier
|
|
204
|
-
: (_e = props.bundleIdentifier) !== null && _e !== void 0 ? _e : `${mainAppBundleId}.${
|
|
223
|
+
: (_e = props.bundleIdentifier) !== null && _e !== void 0 ? _e : `${mainAppBundleId}.${props.type === "clip"
|
|
224
|
+
? // Use a more standardized bundle identifier for App Clips.
|
|
225
|
+
"clip"
|
|
226
|
+
: getSanitizedBundleIdentifier(targetName)}`;
|
|
227
|
+
const deviceFamilies = ((_f = config.ios) === null || _f === void 0 ? void 0 : _f.isTabletOnly)
|
|
228
|
+
? ["tablet"]
|
|
229
|
+
: ((_g = config.ios) === null || _g === void 0 ? void 0 : _g.supportsTablet)
|
|
230
|
+
? ["phone", "tablet"]
|
|
231
|
+
: ["phone"];
|
|
205
232
|
(0, withXcodeChanges_1.withXcodeChanges)(config, {
|
|
206
233
|
configPath: props.configPath,
|
|
207
234
|
name: targetName,
|
|
208
235
|
cwd: "../" +
|
|
209
236
|
path_1.default.relative(config._internal.projectRoot, path_1.default.resolve(props.directory)),
|
|
210
|
-
deploymentTarget: (
|
|
237
|
+
deploymentTarget: (_h = props.deploymentTarget) !== null && _h !== void 0 ? _h : DEFAULT_DEPLOYMENT_TARGET,
|
|
211
238
|
bundleId,
|
|
212
239
|
icon: props.icon,
|
|
213
|
-
|
|
240
|
+
orientation: config.orientation,
|
|
241
|
+
hasAccentColor: !!((_j = props.colors) === null || _j === void 0 ? void 0 : _j.$accent),
|
|
242
|
+
deviceFamilies,
|
|
214
243
|
// @ts-expect-error: who cares
|
|
215
|
-
currentProjectVersion: ((
|
|
244
|
+
currentProjectVersion: ((_k = config.ios) === null || _k === void 0 ? void 0 : _k.buildNumber) || 1,
|
|
216
245
|
frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
|
|
217
246
|
type: props.type,
|
|
218
247
|
teamId: props.appleTeamId,
|
|
219
|
-
exportJs: (
|
|
248
|
+
exportJs: (_l = props.exportJs) !== null && _l !== void 0 ? _l :
|
|
220
249
|
// Assume App Clips are used for React Native.
|
|
221
250
|
props.type === "clip",
|
|
222
251
|
});
|
|
@@ -16,5 +16,8 @@ export type XcodeSettings = {
|
|
|
16
16
|
exportJs?: boolean;
|
|
17
17
|
/** File path to the extension config file. */
|
|
18
18
|
configPath: string;
|
|
19
|
+
orientation?: "default" | "portrait" | "landscape";
|
|
20
|
+
deviceFamilies?: DeviceFamily[];
|
|
19
21
|
};
|
|
22
|
+
export type DeviceFamily = "phone" | "tablet";
|
|
20
23
|
export declare const withXcodeChanges: ConfigPlugin<XcodeSettings>;
|
|
@@ -441,7 +441,7 @@ function createSafariConfigurationList(project, { name, cwd, bundleId, deploymen
|
|
|
441
441
|
});
|
|
442
442
|
return configurationList;
|
|
443
443
|
}
|
|
444
|
-
function createAppClipConfigurationList(project, { name, cwd, bundleId, deploymentTarget, currentProjectVersion, hasAccentColor, }) {
|
|
444
|
+
function createAppClipConfigurationList(project, { name, cwd, bundleId, deploymentTarget, currentProjectVersion, hasAccentColor, orientation, deviceFamilies, }) {
|
|
445
445
|
// TODO: Unify AppIcon and AccentColor logic
|
|
446
446
|
const dynamic = {
|
|
447
447
|
CURRENT_PROJECT_VERSION: currentProjectVersion,
|
|
@@ -469,15 +469,14 @@ function createAppClipConfigurationList(project, { name, cwd, bundleId, deployme
|
|
|
469
469
|
PRODUCT_NAME: "$(TARGET_NAME)",
|
|
470
470
|
SWIFT_EMIT_LOC_STRINGS: "YES",
|
|
471
471
|
SWIFT_VERSION: "5.0",
|
|
472
|
-
|
|
472
|
+
...getDeviceFamilyBuildSettings(deviceFamilies),
|
|
473
473
|
};
|
|
474
474
|
const infoPlist = {
|
|
475
475
|
GENERATE_INFOPLIST_FILE: "YES",
|
|
476
476
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: "YES",
|
|
477
477
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: "YES",
|
|
478
478
|
INFOPLIST_KEY_UILaunchScreen_Generation: "YES",
|
|
479
|
-
|
|
480
|
-
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight",
|
|
479
|
+
...getOrientationBuildSettings(orientation),
|
|
481
480
|
};
|
|
482
481
|
// @ts-expect-error
|
|
483
482
|
const common = {
|
|
@@ -518,6 +517,42 @@ function createAppClipConfigurationList(project, { name, cwd, bundleId, deployme
|
|
|
518
517
|
});
|
|
519
518
|
return configurationList;
|
|
520
519
|
}
|
|
520
|
+
function getOrientationBuildSettings(orientation) {
|
|
521
|
+
// Try to align the orientation with the main app.
|
|
522
|
+
if (orientation === "landscape") {
|
|
523
|
+
return {
|
|
524
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight",
|
|
525
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight",
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
else if (orientation === "portrait") {
|
|
529
|
+
return {
|
|
530
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait",
|
|
531
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown",
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight",
|
|
536
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight",
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function getDeviceFamilyBuildSettings(deviceFamilies) {
|
|
540
|
+
if (!deviceFamilies) {
|
|
541
|
+
return {
|
|
542
|
+
TARGETED_DEVICE_FAMILY: "1,2",
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const families = [];
|
|
546
|
+
if (deviceFamilies.includes("phone")) {
|
|
547
|
+
families.push(1);
|
|
548
|
+
}
|
|
549
|
+
if (deviceFamilies.includes("tablet")) {
|
|
550
|
+
families.push(2);
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
TARGETED_DEVICE_FAMILY: families.join(","),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
521
556
|
function createConfigurationList(project, { name, cwd, bundleId, deploymentTarget, currentProjectVersion, }) {
|
|
522
557
|
const debugBuildConfig = xcode_1.XCBuildConfiguration.create(project, {
|
|
523
558
|
name: "Debug",
|
|
@@ -554,7 +589,7 @@ function createConfigurationList(project, { name, cwd, bundleId, deploymentTarge
|
|
|
554
589
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG",
|
|
555
590
|
SWIFT_EMIT_LOC_STRINGS: "YES",
|
|
556
591
|
SWIFT_OPTIMIZATION_LEVEL: "-Onone",
|
|
557
|
-
SWIFT_VERSION: "5",
|
|
592
|
+
SWIFT_VERSION: "5.0",
|
|
558
593
|
TARGETED_DEVICE_FAMILY: "1,2",
|
|
559
594
|
},
|
|
560
595
|
});
|
|
@@ -593,7 +628,7 @@ function createConfigurationList(project, { name, cwd, bundleId, deploymentTarge
|
|
|
593
628
|
SWIFT_EMIT_LOC_STRINGS: "YES",
|
|
594
629
|
SWIFT_COMPILATION_MODE: "wholemodule",
|
|
595
630
|
SWIFT_OPTIMIZATION_LEVEL: "-O",
|
|
596
|
-
SWIFT_VERSION: "5",
|
|
631
|
+
SWIFT_VERSION: "5.0",
|
|
597
632
|
TARGETED_DEVICE_FAMILY: "1,2",
|
|
598
633
|
},
|
|
599
634
|
});
|
|
@@ -697,7 +732,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
697
732
|
}
|
|
698
733
|
}
|
|
699
734
|
function configureTargetWithEntitlements(target) {
|
|
700
|
-
const entitlements = (0, glob_1.
|
|
735
|
+
const entitlements = (0, glob_1.sync)("*.entitlements", {
|
|
701
736
|
absolute: false,
|
|
702
737
|
cwd: magicCwd,
|
|
703
738
|
});
|
|
@@ -719,7 +754,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
719
754
|
});
|
|
720
755
|
}
|
|
721
756
|
function configureTargetWithPreview(target) {
|
|
722
|
-
const assets = (0, glob_1.
|
|
757
|
+
const assets = (0, glob_1.sync)("preview/*.xcassets", {
|
|
723
758
|
absolute: true,
|
|
724
759
|
cwd: magicCwd,
|
|
725
760
|
})[0];
|
|
@@ -839,7 +874,7 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
839
874
|
fs_1.default.statSync(path_1.default.join(assetsDir, file)).isDirectory())
|
|
840
875
|
.map((file) => path_1.default.join("assets", file));
|
|
841
876
|
const protectedGroup = ensureProtectedGroup(project, path_1.default.dirname(props.cwd));
|
|
842
|
-
const sharedAssets = (0, glob_1.
|
|
877
|
+
const sharedAssets = (0, glob_1.sync)("_shared/*", {
|
|
843
878
|
absolute: false,
|
|
844
879
|
cwd: magicCwd,
|
|
845
880
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacons/apple-targets",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Generate Apple Targets with Expo Prebuild",
|
|
5
5
|
"main": "build/ExtensionStorage.js",
|
|
6
6
|
"types": "build/ExtensionStorage.d.ts",
|
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
"author": "Evan Bacon",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bacons/xcode": "1.0.0-alpha.24",
|
|
37
36
|
"@react-native/normalize-colors": "^0.76.1",
|
|
38
|
-
"glob": "^10.
|
|
37
|
+
"glob": "^10.2.6",
|
|
38
|
+
"@bacons/xcode": "1.0.0-alpha.24",
|
|
39
|
+
"fs-extra": "^11.2.0",
|
|
39
40
|
"debug": "^4.3.4"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/debug": "^4.1.7",
|
|
44
|
+
"@types/fs-extra": "^11.0.4",
|
|
43
45
|
"@types/glob": "^8.1.0",
|
|
44
46
|
"@expo/babel-preset-cli": "^0.3.1",
|
|
45
47
|
"chalk": "^4.0.0",
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const config_plugins_1 = require("expo/config-plugins");
|
|
7
|
-
function generateSwiftModule(config) {
|
|
8
|
-
const { intents } = config;
|
|
9
|
-
const widgetBundleBody = intents
|
|
10
|
-
.map((intent, index) => `// widgetControl${index}()`)
|
|
11
|
-
.join("\n");
|
|
12
|
-
const widgetDefinitions = intents
|
|
13
|
-
.map((intent, index) => {
|
|
14
|
-
return `
|
|
15
|
-
@available(iOS 18.0, *)
|
|
16
|
-
struct widgetControl${index}: ControlWidget {
|
|
17
|
-
static let kind: String = "${intent.kind}"
|
|
18
|
-
var body: some ControlWidgetConfiguration {
|
|
19
|
-
StaticControlConfiguration(kind: Self.kind) {
|
|
20
|
-
ControlWidgetButton(action: OpenAppIntent${index}()) {
|
|
21
|
-
Label("${intent.label}", ${
|
|
22
|
-
// If the icon is generated, use the image system
|
|
23
|
-
intent.icon.startsWith("generated_expo_intent_icon_")
|
|
24
|
-
? "image"
|
|
25
|
-
: "systemImage"}: "${intent.icon}")
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
.displayName("${intent.displayName}")
|
|
29
|
-
.description("${intent.description}")
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// This must be in both targets when \`openAppWhenRun = true\`
|
|
34
|
-
// https://developer.apple.com/forums/thread/763851
|
|
35
|
-
@available(iOS 18.0, *)
|
|
36
|
-
struct OpenAppIntent${index}: ControlConfigurationIntent {
|
|
37
|
-
static let title: LocalizedStringResource = "${intent.displayName}"
|
|
38
|
-
static let description = IntentDescription(stringLiteral: "${intent.description}")
|
|
39
|
-
static let isDiscoverable = true
|
|
40
|
-
static let openAppWhenRun: Bool = true
|
|
41
|
-
|
|
42
|
-
@MainActor
|
|
43
|
-
func perform() async throws -> some IntentResult & OpensIntent {
|
|
44
|
-
return .result(opensIntent: OpenURLIntent(URL(string: "${intent.url}")!))
|
|
45
|
-
}
|
|
46
|
-
}`;
|
|
47
|
-
})
|
|
48
|
-
.join("\n");
|
|
49
|
-
return `
|
|
50
|
-
// Generated by @bacons/apple-targets via the appIntents option in the config file.
|
|
51
|
-
import AppIntents
|
|
52
|
-
import SwiftUI
|
|
53
|
-
import WidgetKit
|
|
54
|
-
|
|
55
|
-
// TODO: These must be added to the WidgetBundle manually. They need to be linked outside of the _shared folder.
|
|
56
|
-
// @main
|
|
57
|
-
// struct exportWidgets: WidgetBundle {
|
|
58
|
-
// var body: some Widget {
|
|
59
|
-
${widgetBundleBody}
|
|
60
|
-
// }
|
|
61
|
-
// }
|
|
62
|
-
${widgetDefinitions}
|
|
63
|
-
`;
|
|
64
|
-
}
|
|
65
|
-
const fs_1 = __importDefault(require("fs"));
|
|
66
|
-
const withLinkAppIntent = (config, { targetRoot, intents }) => {
|
|
67
|
-
(0, config_plugins_1.withDangerousMod)(config, [
|
|
68
|
-
"ios",
|
|
69
|
-
async (config) => {
|
|
70
|
-
const sharedFile = `${targetRoot}/_shared/generated-intents.swift`;
|
|
71
|
-
await fs_1.default.promises.mkdir(`${targetRoot}/_shared`, { recursive: true });
|
|
72
|
-
await fs_1.default.promises.writeFile(sharedFile, generateSwiftModule({
|
|
73
|
-
intents: intents.map((intent, index) => {
|
|
74
|
-
var _a;
|
|
75
|
-
return ({
|
|
76
|
-
...intent,
|
|
77
|
-
kind: (_a = intent.kind) !== null && _a !== void 0 ? _a : `${config.ios.bundleIdentifier}.${index}`,
|
|
78
|
-
});
|
|
79
|
-
}),
|
|
80
|
-
}), "utf-8");
|
|
81
|
-
return config;
|
|
82
|
-
},
|
|
83
|
-
]);
|
|
84
|
-
return config;
|
|
85
|
-
};
|
|
86
|
-
exports.default = withLinkAppIntent;
|