-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Improve live activity routing and diagnostics #3685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9c3eb7b
73e2d1d
5ad2eea
34338a8
870cc6f
4ed4a4c
2c68eb1
e694f66
990372a
011a09b
95e4ac2
bf5de3c
0c5ffc5
a2e0830
2e02973
558a811
41296c5
617a88c
fd82469
ed4c4ec
42ad1f1
4e2a475
387d068
97ed442
be4e710
6f0b32d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| "use strict"; | ||
|
|
||
| // Bundle the widget asset catalog into an app-extension (widget) target. | ||
| // | ||
| // expo-widgets generates the widget target without a Resources build phase, and | ||
| // hand-adding a PBXResourcesBuildPhase does NOT get picked up — xcodebuild's | ||
| // planner never schedules `actool` for it (verified: even a full re-plan skips | ||
| // it). So instead we add a shell-script phase that runs `actool` directly and | ||
| // drops the compiled Assets.car into the extension bundle. Marked | ||
| // alwaysOutOfDate so the build system always runs it. | ||
| // | ||
| // Idempotent across re-runs. Returns true when it added the phase, false when it | ||
| // was already present or the target was not found. | ||
|
|
||
| const PHASE_NAME = "Compile Widget Assets"; | ||
|
|
||
| // Compiles ExpoWidgetsTarget/Assets.xcassets into the extension's resources dir. | ||
| // Uses only Xcode-provided build settings so it works for device + simulator. | ||
| const ACTOOL_SCRIPT = [ | ||
| "set -e", | ||
| 'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"', | ||
| 'if [ ! -d "$CATALOG" ]; then', | ||
| ' echo "warning: widget asset catalog not found at $CATALOG"', | ||
| " exit 0", | ||
| "fi", | ||
| 'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"', | ||
| 'mkdir -p "$DEST"', | ||
| 'xcrun actool "$CATALOG" --compile "$DEST" --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" --output-format human-readable-text', | ||
| ].join("\n"); | ||
|
|
||
| function stripComments(map) { | ||
| const out = {}; | ||
| for (const key of Object.keys(map || {})) { | ||
| if (key.endsWith("_comment")) continue; | ||
| out[key] = map[key]; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| function findByName(map, name) { | ||
| for (const [uuid, value] of Object.entries(stripComments(map))) { | ||
| if (value && value.name === name) return { uuid, value }; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * @param {import('xcode').XcodeProject} proj | ||
| * @param {{ targetName: string }} opts | ||
| */ | ||
| function addWidgetAssetCatalog(proj, opts) { | ||
| const objects = proj.hash.project.objects; | ||
| const target = findByName(objects.PBXNativeTarget, opts.targetName); | ||
| if (!target) return false; | ||
|
|
||
| const phases = target.value.buildPhases || []; | ||
| const existing = objects.PBXShellScriptBuildPhase || {}; | ||
| const already = Object.entries(stripComments(existing)).some( | ||
| ([uuid, value]) => | ||
| value && value.name === `"${PHASE_NAME}"` && phases.some((p) => p.value === uuid), | ||
| ); | ||
| if (already) return false; | ||
|
|
||
| const { uuid } = proj.addBuildPhase([], "PBXShellScriptBuildPhase", PHASE_NAME, target.uuid, { | ||
| shellPath: "/bin/sh", | ||
| shellScript: ACTOOL_SCRIPT, | ||
| }); | ||
| // Always run: input-analysis is exactly what skipped the Resources phase. | ||
| objects.PBXShellScriptBuildPhase[uuid].alwaysOutOfDate = 1; | ||
| return true; | ||
| } | ||
|
|
||
| module.exports = { addWidgetAssetCatalog }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| "use strict"; | ||
|
|
||
| // Ships the branded T3 mark to the Live Activity / widget extension. | ||
| // | ||
| // expo-widgets generates ExpoWidgetsTarget without a Resources build phase and | ||
| // has no asset support, so this plugin (a) writes an SVG template image set into | ||
| // the generated widget asset catalog and (b) wires that catalog into the widget | ||
| // target with a dedicated Resources build phase. Both steps are idempotent and | ||
| // survive `expo prebuild --clean`. Must be listed AFTER "expo-widgets" in the | ||
| // plugins array so the widget target exists when this runs. | ||
|
|
||
| const path = require("path"); | ||
| const fs = require("fs"); | ||
| const { withDangerousMod, withXcodeProject } = require("expo/config-plugins"); | ||
| const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs"); | ||
|
|
||
| const TARGET_NAME = "ExpoWidgetsTarget"; | ||
| const CATALOG_NAME = "Assets.xcassets"; | ||
| const IMAGE_SET = "T3Mark.imageset"; | ||
| const SVG_NAME = "T3Mark.svg"; | ||
|
|
||
| const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n"; | ||
| const IMAGE_SET_CONTENTS = | ||
| JSON.stringify( | ||
| { | ||
| images: [{ idiom: "universal", filename: SVG_NAME }], | ||
| info: { author: "expo", version: 1 }, | ||
| properties: { | ||
| "preserves-vector-representation": true, | ||
| "template-rendering-intent": "template", | ||
| }, | ||
| }, | ||
| null, | ||
| 2, | ||
| ) + "\n"; | ||
|
|
||
| function withAssetFiles(config) { | ||
| return withDangerousMod(config, [ | ||
| "ios", | ||
| (cfg) => { | ||
| const source = path.join(cfg.modRequest.projectRoot, "assets", "widget", SVG_NAME); | ||
| const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME); | ||
| const imageSetDir = path.join(catalogDir, IMAGE_SET); | ||
| fs.mkdirSync(imageSetDir, { recursive: true }); | ||
| fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS); | ||
| fs.writeFileSync(path.join(imageSetDir, "Contents.json"), IMAGE_SET_CONTENTS); | ||
| fs.copyFileSync(source, path.join(imageSetDir, SVG_NAME)); | ||
| return cfg; | ||
| }, | ||
| ]); | ||
| } | ||
|
|
||
| function withAssetWiring(config) { | ||
| return withXcodeProject(config, (cfg) => { | ||
| addWidgetAssetCatalog(cfg.modResults, { targetName: TARGET_NAME }); | ||
| return cfg; | ||
| }); | ||
| } | ||
|
|
||
| module.exports = function withWidgetLogoAsset(config) { | ||
| return withAssetWiring(withAssetFiles(config)); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| "use strict"; | ||
|
|
||
| // One-off: apply the widget asset-catalog wiring to the already-generated | ||
| // ios/ project so the current build compiles ExpoWidgetsTarget/Assets.xcassets | ||
| // without a full `expo prebuild`. The durable equivalent lives in | ||
| // plugins/withWidgetLogoAsset.cjs and runs on prebuild. | ||
|
|
||
| const path = require("path"); | ||
| const fs = require("fs"); | ||
|
|
||
| const xcodePath = require.resolve("xcode", { | ||
| paths: [ | ||
| require.resolve("@expo/config-plugins", { paths: [require.resolve("expo/package.json")] }), | ||
| ], | ||
| }); | ||
| const xcode = require(xcodePath); | ||
| const { addWidgetAssetCatalog } = require("../plugins/lib/addWidgetAssetCatalog.cjs"); | ||
|
|
||
| const pbxprojPath = path.join(__dirname, "..", "ios", "T3CodeDev.xcodeproj", "project.pbxproj"); | ||
| const proj = xcode.project(pbxprojPath); | ||
| proj.parseSync(); | ||
|
|
||
| const added = addWidgetAssetCatalog(proj, { targetName: "ExpoWidgetsTarget" }); | ||
|
|
||
| if (added) { | ||
| fs.writeFileSync(pbxprojPath, proj.writeSync()); | ||
| console.log("Added widget asset-compile phase to ExpoWidgetsTarget."); | ||
| } else { | ||
| console.log("No change: phase already present or target not found."); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,20 @@ import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay"; | |
|
|
||
| import type { Preferences } from "../../lib/storage"; | ||
|
|
||
| // Development builds are Xcode-signed and receive sandbox APNs tokens; | ||
| // preview and production builds are distribution-signed and use production | ||
| // APNs. The relay routes each device's pushes accordingly. | ||
| export function resolveApsEnvironment(appVariant: unknown): "sandbox" | "production" { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| return appVariant === "development" ? "sandbox" : "production"; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| export function makeRelayDeviceRegistrationRequest(input: { | ||
| readonly deviceId: string; | ||
| readonly label: string; | ||
| readonly iosMajorVersion: number; | ||
| readonly appVersion?: string; | ||
| readonly bundleId?: string; | ||
| readonly apsEnvironment?: "sandbox" | "production"; | ||
| readonly pushToken?: string; | ||
| readonly pushToStartToken?: string; | ||
| readonly notificationsEnabled: boolean; | ||
|
|
@@ -19,6 +28,8 @@ export function makeRelayDeviceRegistrationRequest(input: { | |
| platform: "ios", | ||
| iosMajorVersion: input.iosMajorVersion, | ||
| appVersion: input.appVersion, | ||
| ...(input.bundleId ? { bundleId: input.bundleId } : {}), | ||
| ...(input.apsEnvironment ? { apsEnvironment: input.apsEnvironment } : {}), | ||
| ...(input.pushToken ? { pushToken: input.pushToken } : {}), | ||
| ...(input.pushToStartToken ? { pushToStartToken: input.pushToStartToken } : {}), | ||
| preferences: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
t3code/apps/mobile/app.config.ts
Lines 96 to 97 in 5086c14
The
frequentUpdates: trueoption under theexpo-widgetsplugin does not add the requiredNSSupportsLiveActivitiesFrequentUpdateskey toInfo.plist, so iOS keeps the standard Live Activity update budget and frequent updates are never enabled. SetNSSupportsLiveActivitiesFrequentUpdates: truein theios.infoPlistblock to actually grant the entitlement.infoPlist: { + NSSupportsLiveActivitiesFrequentUpdates: true, NSAppTransportSecurity: {🚀 Reply "fix it for me" or copy this AI Prompt for your agent: