@bacons/apple-targets 0.1.16 → 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.d.ts +1 -0
- package/build/withWidget.js +61 -8
- package/build/withXcodeChanges.d.ts +3 -0
- package/build/withXcodeChanges.js +39 -4
- package/package.json +1 -1
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.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type Entitlements = Partial<{
|
|
|
59
59
|
"com.apple.developer.driverkit.transport.hid": boolean;
|
|
60
60
|
"com.apple.developer.driverkit.family.audio": boolean;
|
|
61
61
|
"com.apple.developer.shared-with-you": boolean;
|
|
62
|
+
"com.apple.developer.associated-domains": string[];
|
|
62
63
|
}>;
|
|
63
64
|
export type Config = {
|
|
64
65
|
/**
|
package/build/withWidget.js
CHANGED
|
@@ -51,7 +51,7 @@ function kebabToCamelCase(str) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
const withWidget = (config, props) => {
|
|
54
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
54
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
55
55
|
prebuildLogQueue.add(() => warnOnce((0, chalk_1.default) `\nUsing experimental Config Plugin {bold @bacons/apple-targets} that is subject to breaking changes.`));
|
|
56
56
|
// TODO: Magically based on the top-level folders in the `ios-widgets/` folder
|
|
57
57
|
if (props.icon && !/https?:\/\//.test(props.icon)) {
|
|
@@ -74,11 +74,54 @@ const withWidget = (config, props) => {
|
|
|
74
74
|
if (entitlementsJson) {
|
|
75
75
|
// Apply default entitlements that must be present for a target to work.
|
|
76
76
|
const applyDefaultEntitlements = (entitlements) => {
|
|
77
|
-
var _a, _b;
|
|
77
|
+
var _a, _b, _c, _d, _e, _f;
|
|
78
78
|
if (props.type === "clip") {
|
|
79
79
|
entitlements["com.apple.developer.parent-application-identifiers"] = [
|
|
80
80
|
`$(AppIdentifierPrefix)${config.ios.bundleIdentifier}`,
|
|
81
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
|
+
}
|
|
82
125
|
// NOTE: This doesn't seem to be required anymore (Oct 12 2024):
|
|
83
126
|
// entitlements["com.apple.developer.on-demand-install-capable"] = true;
|
|
84
127
|
}
|
|
@@ -89,7 +132,7 @@ const withWidget = (config, props) => {
|
|
|
89
132
|
!hasDefinedAppGroupsManually &&
|
|
90
133
|
// And the target is part of a predefined list of types that benefit from app groups that match the main app...
|
|
91
134
|
target_1.SHOULD_USE_APP_GROUPS_BY_DEFAULT[props.type]) {
|
|
92
|
-
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];
|
|
93
136
|
if (Array.isArray(mainAppGroups) && mainAppGroups.length > 0) {
|
|
94
137
|
// Then set the target app groups to match the main app.
|
|
95
138
|
entitlements[APP_GROUP_KEY] = mainAppGroups;
|
|
@@ -177,22 +220,32 @@ const withWidget = (config, props) => {
|
|
|
177
220
|
const mainAppBundleId = config.ios.bundleIdentifier;
|
|
178
221
|
const bundleId = ((_d = props.bundleIdentifier) === null || _d === void 0 ? void 0 : _d.startsWith("."))
|
|
179
222
|
? mainAppBundleId + props.bundleIdentifier
|
|
180
|
-
: (_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"];
|
|
181
232
|
(0, withXcodeChanges_1.withXcodeChanges)(config, {
|
|
182
233
|
configPath: props.configPath,
|
|
183
234
|
name: targetName,
|
|
184
235
|
cwd: "../" +
|
|
185
236
|
path_1.default.relative(config._internal.projectRoot, path_1.default.resolve(props.directory)),
|
|
186
|
-
deploymentTarget: (
|
|
237
|
+
deploymentTarget: (_h = props.deploymentTarget) !== null && _h !== void 0 ? _h : DEFAULT_DEPLOYMENT_TARGET,
|
|
187
238
|
bundleId,
|
|
188
239
|
icon: props.icon,
|
|
189
|
-
|
|
240
|
+
orientation: config.orientation,
|
|
241
|
+
hasAccentColor: !!((_j = props.colors) === null || _j === void 0 ? void 0 : _j.$accent),
|
|
242
|
+
deviceFamilies,
|
|
190
243
|
// @ts-expect-error: who cares
|
|
191
|
-
currentProjectVersion: ((
|
|
244
|
+
currentProjectVersion: ((_k = config.ios) === null || _k === void 0 ? void 0 : _k.buildNumber) || 1,
|
|
192
245
|
frameworks: (0, target_1.getFrameworksForType)(props.type).concat(props.frameworks || []),
|
|
193
246
|
type: props.type,
|
|
194
247
|
teamId: props.appleTeamId,
|
|
195
|
-
exportJs: (
|
|
248
|
+
exportJs: (_l = props.exportJs) !== null && _l !== void 0 ? _l :
|
|
196
249
|
// Assume App Clips are used for React Native.
|
|
197
250
|
props.type === "clip",
|
|
198
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",
|