| author | |
| committer | |
| log | 00da6b57a43aef39162ff9e8205b5441c24298d0 |
| tree | 863f74a0cbd1a59bb24dfdfbb9c7c50097691bf8 |
| parent | 6ab2f16809e584449831aa5dba0b0e5ca14a2a54 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 18 | import { createIncomingSharePayloadReader } from "./incoming-share-native"; | ||
| 18 | import { IncomingShareInbox } from "./incoming-share-inbox"; | 19 | import { IncomingShareInbox } from "./incoming-share-inbox"; |
| 19 | import { | 20 | import { |
| 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 | } |
| 50 | 51 | ||
| 52 | const getIncomingSharePayloads = createIncomingSharePayloadReader({ | ||
| 53 | platform: Platform.OS, | ||
| 54 | readPayloads: getSharedPayloads, | ||
| 55 | }); | ||
| 56 | |||
| 51 | async function resolvedPayloadsForImages(): Promise<ReadonlyArray<ResolvedSharePayload>> { | 57 | async 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 @@ | ||
| 1 | import { describe, expect, it, vi } from "@effect/vitest"; | ||
| 2 | import type { SharePayload } from "expo-sharing"; | ||
| 3 | |||
| 4 | import { createIncomingSharePayloadReader } from "./incoming-share-native"; | ||
| 5 | |||
| 6 | const PAYLOAD: SharePayload = { | ||
| 7 | shareType: "text", | ||
| 8 | mimeType: "text/plain", | ||
| 9 | value: "Fix this", | ||
| 10 | }; | ||
| 11 | |||
| 12 | describe("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 @@ | ||
| 1 | import type { SharePayload } from "expo-sharing"; | ||
| 2 | |||
| 3 | const IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE = "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID"; | ||
| 4 | |||
| 5 | function 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 | */ | ||
| 16 | export 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 | } | ||