authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-30 01:48:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 01:48:16-07:00
log00da6b57a43aef39162ff9e8205b5441c24298d0
tree863f74a0cbd1a59bb24dfdfbb9c7c50097691bf8
parent6ab2f16809e584449831aa5dba0b0e5ca14a2a54
signature Signed by PGP key B5690EEEBB952194

fix(mobile): stop shared content errors in Personal Team builds (#4943)


3 files changed, 90 insertions(+), 1 deletions(-)

apps/mobile/src/features/sharing/IncomingShareProvider.tsx+7-1
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15 type IncomingShareDestination,15 type IncomingShareDestination,
16 type IncomingShareDraft,16 type IncomingShareDraft,
17} from "./incoming-share-model";17} from "./incoming-share-model";
18import { createIncomingSharePayloadReader } from "./incoming-share-native";
18import { IncomingShareInbox } from "./incoming-share-inbox";19import { IncomingShareInbox } from "./incoming-share-inbox";
19import {20import {
20 loadIncomingShareDrafts,21 loadIncomingShareDrafts,
...@@ -48,6 +49,11 @@ function receiveSharingEnabled(): boolean {...@@ -48,6 +49,11 @@ function receiveSharingEnabled(): boolean {
48 return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true;49 return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true;
49}50}
5051
52const getIncomingSharePayloads = createIncomingSharePayloadReader({
53 platform: Platform.OS,
54 readPayloads: getSharedPayloads,
55});
56
51async function resolvedPayloadsForImages(): Promise<ReadonlyArray<ResolvedSharePayload>> {57async function resolvedPayloadsForImages(): Promise<ReadonlyArray<ResolvedSharePayload>> {
52 try {58 try {
53 return await getResolvedSharedPayloadsAsync();59 return await getResolvedSharedPayloadsAsync();
...@@ -121,7 +127,7 @@ const incomingShareInbox = new IncomingShareInbox({...@@ -121,7 +127,7 @@ const incomingShareInbox = new IncomingShareInbox({
121 loadDrafts: loadIncomingShareDrafts,127 loadDrafts: loadIncomingShareDrafts,
122 writeDraft: writeIncomingShareDraft,128 writeDraft: writeIncomingShareDraft,
123 removeDraft: removeIncomingShareDraft,129 removeDraft: removeIncomingShareDraft,
124 getPayloads: getSharedPayloads,130 getPayloads: getIncomingSharePayloads,
125 clearPayloads: clearSharedPayloads,131 clearPayloads: clearSharedPayloads,
126 buildDraft: async ({ payloads, id, createdAt }) => {132 buildDraft: async ({ payloads, id, createdAt }) => {
127 const cleanupUris = new Set<string>();133 const cleanupUris = new Set<string>();
apps/mobile/src/features/sharing/incoming-share-native.test.ts created+46
...@@ -0,0 +1,46 @@
1import { describe, expect, it, vi } from "@effect/vitest";
2import type { SharePayload } from "expo-sharing";
3
4import { createIncomingSharePayloadReader } from "./incoming-share-native";
5
6const PAYLOAD: SharePayload = {
7 shareType: "text",
8 mimeType: "text/plain",
9 value: "Fix this",
10};
11
12describe("createIncomingSharePayloadReader", () => {
13 it("treats a missing iOS App Group as an unavailable inbox", () => {
14 const readPayloads = vi.fn((): ReadonlyArray<SharePayload> => {
15 throw Object.assign(new Error("Expo-sharing has failed to fetch the app group id"), {
16 code: "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID",
17 });
18 });
19 const read = createIncomingSharePayloadReader({ platform: "ios", readPayloads });
20
21 expect(read()).toEqual([]);
22 expect(read()).toEqual([]);
23 expect(readPayloads).toHaveBeenCalledTimes(1);
24 });
25
26 it("returns native payloads when receiving is available", () => {
27 const read = createIncomingSharePayloadReader({
28 platform: "ios",
29 readPayloads: () => [PAYLOAD],
30 });
31
32 expect(read()).toEqual([PAYLOAD]);
33 });
34
35 it("preserves other native errors", () => {
36 const error = new Error("native failure");
37 const read = createIncomingSharePayloadReader({
38 platform: "ios",
39 readPayloads: () => {
40 throw error;
41 },
42 });
43
44 expect(read).toThrow(error);
45 });
46});
apps/mobile/src/features/sharing/incoming-share-native.ts created+37
...@@ -0,0 +1,37 @@
1import type { SharePayload } from "expo-sharing";
2
3const IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE = "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID";
4
5function errorCode(error: unknown): string | null {
6 if (typeof error !== "object" || error === null || !("code" in error)) {
7 return null;
8 }
9 return typeof error.code === "string" ? error.code : null;
10}
11
12/**
13 * Normalizes the native "share into" capability to an empty inbox. Personal
14 * Team builds cannot include the App Group that expo-sharing reads from.
15 */
16export function createIncomingSharePayloadReader(input: {
17 readonly platform: string;
18 readonly readPayloads: () => ReadonlyArray<SharePayload>;
19}): () => ReadonlyArray<SharePayload> {
20 let isUnavailable = false;
21
22 return () => {
23 if (isUnavailable) {
24 return [];
25 }
26
27 try {
28 return input.readPayloads();
29 } catch (error) {
30 if (input.platform === "ios" && errorCode(error) === IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE) {
31 isUnavailable = true;
32 return [];
33 }
34 throw error;
35 }
36 };
37}