From a11d3753322e4b8cdcc11b902b00f4f31f92143f Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 01:40:07 -0700 Subject: [PATCH] fix(mobile): stop shared content errors in Personal Team builds --- .../sharing/IncomingShareProvider.tsx | 8 +++- .../sharing/incoming-share-native.test.ts | 46 +++++++++++++++++++ .../features/sharing/incoming-share-native.ts | 37 +++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/src/features/sharing/incoming-share-native.test.ts create mode 100644 apps/mobile/src/features/sharing/incoming-share-native.ts diff --git a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx index 04d371e5d2f..9203e665190 100644 --- a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx +++ b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx @@ -15,6 +15,7 @@ import { type IncomingShareDestination, type IncomingShareDraft, } from "./incoming-share-model"; +import { createIncomingSharePayloadReader } from "./incoming-share-native"; import { IncomingShareInbox } from "./incoming-share-inbox"; import { loadIncomingShareDrafts, @@ -48,6 +49,11 @@ function receiveSharingEnabled(): boolean { return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true; } +const getIncomingSharePayloads = createIncomingSharePayloadReader({ + platform: Platform.OS, + readPayloads: getSharedPayloads, +}); + async function resolvedPayloadsForImages(): Promise> { try { return await getResolvedSharedPayloadsAsync(); @@ -121,7 +127,7 @@ const incomingShareInbox = new IncomingShareInbox({ loadDrafts: loadIncomingShareDrafts, writeDraft: writeIncomingShareDraft, removeDraft: removeIncomingShareDraft, - getPayloads: getSharedPayloads, + getPayloads: getIncomingSharePayloads, clearPayloads: clearSharedPayloads, buildDraft: async ({ payloads, id, createdAt }) => { const cleanupUris = new Set(); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.test.ts b/apps/mobile/src/features/sharing/incoming-share-native.test.ts new file mode 100644 index 00000000000..8511f61ffa2 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "@effect/vitest"; +import type { SharePayload } from "expo-sharing"; + +import { createIncomingSharePayloadReader } from "./incoming-share-native"; + +const PAYLOAD: SharePayload = { + shareType: "text", + mimeType: "text/plain", + value: "Fix this", +}; + +describe("createIncomingSharePayloadReader", () => { + it("treats a missing iOS App Group as an unavailable inbox", () => { + const readPayloads = vi.fn((): ReadonlyArray => { + throw Object.assign(new Error("Expo-sharing has failed to fetch the app group id"), { + code: "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID", + }); + }); + const read = createIncomingSharePayloadReader({ platform: "ios", readPayloads }); + + expect(read()).toEqual([]); + expect(read()).toEqual([]); + expect(readPayloads).toHaveBeenCalledTimes(1); + }); + + it("returns native payloads when receiving is available", () => { + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => [PAYLOAD], + }); + + expect(read()).toEqual([PAYLOAD]); + }); + + it("preserves other native errors", () => { + const error = new Error("native failure"); + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => { + throw error; + }, + }); + + expect(read).toThrow(error); + }); +}); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.ts b/apps/mobile/src/features/sharing/incoming-share-native.ts new file mode 100644 index 00000000000..b6da9534c92 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.ts @@ -0,0 +1,37 @@ +import type { SharePayload } from "expo-sharing"; + +const IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE = "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID"; + +function errorCode(error: unknown): string | null { + if (typeof error !== "object" || error === null || !("code" in error)) { + return null; + } + return typeof error.code === "string" ? error.code : null; +} + +/** + * Normalizes the native "share into" capability to an empty inbox. Personal + * Team builds cannot include the App Group that expo-sharing reads from. + */ +export function createIncomingSharePayloadReader(input: { + readonly platform: string; + readonly readPayloads: () => ReadonlyArray; +}): () => ReadonlyArray { + let isUnavailable = false; + + return () => { + if (isUnavailable) { + return []; + } + + try { + return input.readPayloads(); + } catch (error) { + if (input.platform === "ios" && errorCode(error) === IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE) { + isUnavailable = true; + return []; + } + throw error; + } + }; +}