@bacons/apple-targets 0.1.17 → 0.1.18
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 +77 -29
- 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 +42 -7
- package/package.json +5 -3
- package/build/withLinkAppIntent.d.ts +0 -7
- package/build/withLinkAppIntent.js +0 -86
package/README.md
CHANGED
|
@@ -405,43 +405,91 @@ let index = defaults?.string(forKey: "myKey")
|
|
|
405
405
|
|
|
406
406
|
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
407
|
|
|
408
|
-
##
|
|
408
|
+
## App Clips
|
|
409
409
|
|
|
410
|
-
|
|
410
|
+
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
411
|
|
|
412
|
-
|
|
412
|
+
Here are a few notes from my experience building https://pillarvalley.expo.app (open on iOS to test).
|
|
413
413
|
|
|
414
|
-
|
|
414
|
+
Build the app first, then the website. You can always instantly update the website if it's wrong. This includes the AASA, and metadata.
|
|
415
|
+
|
|
416
|
+
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.
|
|
417
|
+
|
|
418
|
+
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.
|
|
419
|
+
|
|
420
|
+
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.
|
|
421
|
+
|
|
422
|
+
Ensure all the build numbers are the same across the `CURRENT_PROJECT_VERSION` and `CFBundleVersion` (Info.plist) otherwise the app will fail to build.
|
|
423
|
+
|
|
424
|
+
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).
|
|
425
|
+
|
|
426
|
+
The value will be `<Apple Team ID>.<App Clip Bundle ID>`:
|
|
427
|
+
|
|
428
|
+
```
|
|
415
429
|
{
|
|
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
|
-
];
|
|
430
|
+
"appclips": {
|
|
431
|
+
"apps": ["QQ57RJ5UTD.com.evanbacon.pillarvalley.clip"]
|
|
432
|
+
}
|
|
429
433
|
}
|
|
430
434
|
```
|
|
431
435
|
|
|
432
|
-
|
|
436
|
+
Add the website URL to your App Clip entitlements (not the main entitlements). Here's an example with `https://pillarvalley.expo.app`:
|
|
433
437
|
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
438
|
+
```xml
|
|
439
|
+
<key>com.apple.developer.associated-domains</key>
|
|
440
|
+
<array>
|
|
441
|
+
<string>appclips:pillarvalley.expo.app</string>
|
|
442
|
+
</array>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
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).
|
|
446
|
+
|
|
447
|
+
You should handle redirection from this default URL too with a [`app/+native-intent.ts`](https://docs.expo.dev/router/advanced/native-intent/) file:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
export function redirectSystemPath({ path }: { path: string }): string {
|
|
451
|
+
try {
|
|
452
|
+
// Handle App Clip default page redirection.
|
|
453
|
+
// If path matches https://appclip.apple.com/id?p=com.evanbacon.pillarvalley.clip (with any query parameters), then redirect to `/` path.
|
|
454
|
+
const url = new URL(path);
|
|
455
|
+
if (url.hostname === "appclip.apple.com") {
|
|
456
|
+
// Redirect to the root path and make the original URL available as a query parameter (optional).
|
|
457
|
+
return "/?ref=" + encodeURIComponent(path);
|
|
458
|
+
}
|
|
459
|
+
return path;
|
|
460
|
+
} catch {
|
|
461
|
+
return path;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
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.
|
|
469
|
+
|
|
470
|
+
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:
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
<meta
|
|
474
|
+
name="apple-itunes-app"
|
|
475
|
+
content={
|
|
476
|
+
"app-id=1336398804, app-clip-bundle-id=com.evanbacon.pillarvalley.clip, app-clip-display=card"
|
|
477
|
+
}
|
|
478
|
+
/>
|
|
443
479
|
```
|
|
444
480
|
|
|
445
|
-
You should
|
|
481
|
+
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)).
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
<Head>
|
|
485
|
+
{/* Required for app clips: */}
|
|
486
|
+
{/* https://developer.apple.com/documentation/appclip/supporting-invocations-from-your-website-and-the-messages-app */}
|
|
487
|
+
<meta property="og:image" content="https://pillarvalley.expo.app/og.png" />
|
|
488
|
+
</Head>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
You also need a `1800x1200` image for the App Store Connect image preview, so make both of these images around the same time.
|
|
492
|
+
|
|
493
|
+
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
494
|
|
|
447
|
-
|
|
495
|
+
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",
|
|
@@ -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.18",
|
|
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;
|