Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/components/ConduitActionsContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React from "react";
import { Platform } from "react-native";
import { ReactTestRenderer, act, create } from "react-test-renderer";

import {
Expand Down Expand Up @@ -59,13 +60,21 @@ const mockedUseHostedExperienceState =
>;

describe("ConduitActionsProvider", () => {
const originalPlatformOs = Platform.OS;
const originalPlatformConstants = Platform.constants;

beforeEach(() => {
setPlatform("ios", { interfaceIdiom: "phone" });
});

afterEach(() => {
jest.clearAllMocks();
mountedRenderers.splice(0).forEach((renderer) => {
act(() => {
renderer.unmount();
});
});
setPlatform(originalPlatformOs, originalPlatformConstants);
});

it("does not use the local Android personal compartment ID on iOS", () => {
Expand Down Expand Up @@ -98,6 +107,43 @@ describe("ConduitActionsProvider", () => {
expect(contextValue.isPersonalPairingPreparing).toBe(true);
});

it("uses the local personal compartment ID on Mac Catalyst", () => {
setPlatform("ios", {
...originalPlatformConstants,
interfaceIdiom: "mac",
isMacCatalyst: true,
});
mockedUseHostedExperienceState.mockReturnValue({
authPhase: "authenticated",
session: { personalPairingWrapperBaseUrl: null },
stationPhase: "active",
entitlementSnapshot: "active",
conduitsSnapshot: {
entitlement: { status: "active" },
conduits: [
{
conduit_id: "cond_1",
proxy_id: "st_1",
status: "active",
},
],
},
} as never);

const queryClient = createTestQueryClient();
queryClient.setQueryData(
[QUERYKEY_ANDROID_PERSONAL_COMPARTMENT_ID],
"jgr+fj3yz6Wpn/vV7qlP4Sh+hBkThZCDEe6+OVJEm2g",
);

const contextValue = renderProvider(queryClient);

expect(contextValue.personalCompartmentId).toBe(
"jgr+fj3yz6Wpn/vV7qlP4Sh+hBkThZCDEe6+OVJEm2g",
);
expect(contextValue.isPersonalPairingPreparing).toBe(false);
});

it("uses the hosted snapshot personal compartment ID on iOS once it is ready", () => {
mockedUseHostedExperienceState.mockReturnValue({
authPhase: "authenticated",
Expand Down Expand Up @@ -171,3 +217,14 @@ function createTestQueryClient(): QueryClient {
},
});
}

function setPlatform(os: typeof Platform.OS, constants: object) {
Object.defineProperty(Platform, "OS", {
configurable: true,
value: os,
});
Object.defineProperty(Platform, "constants", {
configurable: true,
value: constants,
});
}
30 changes: 18 additions & 12 deletions src/components/ConduitActionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import {
RyveClaimMaterial,
resolvePreferredRyveName,
} from "@/src/components/ryveClaim";
import { useAndroidPersonalCompartmentId, useConduitName } from "@/src/hooks";
import { useConduitName, useLocalPersonalCompartmentId } from "@/src/hooks";
import { useHostedExperienceState } from "@/src/hosted/experience/hooks";
import { isEntitlementAllowed } from "@/src/hosted/experience/stateMachine";
import { resolveHostedPersonalPairingState } from "@/src/hosted/personalPairing";
import { isLocalPersonalPairingSupported } from "@/src/personalCompartmentId";

export interface ConduitActionsContextValue {
/** Open the Ryve claim modal for a given claim material */
Expand Down Expand Up @@ -63,13 +64,16 @@ export function useConduitActions(): ConduitActionsContextValue {

export function ConduitActionsProvider({ children }: React.PropsWithChildren) {
const { openModal } = useModal();
const { data: androidPersonalCompartmentId } =
useAndroidPersonalCompartmentId();
const localPersonalCompartmentIdQuery = useLocalPersonalCompartmentId();
const localPersonalCompartmentId = localPersonalCompartmentIdQuery.data;
const { data: conduitName } = useConduitName();
const state = useHostedExperienceState();
const conduits = state.conduitsSnapshot?.conduits ?? [];
const hasHostedSession =
state.authPhase === "authenticated" && state.session != null;
const localPersonalPairingSupported = isLocalPersonalPairingSupported();
const hostedPairingOnly =
Platform.OS === "ios" && !localPersonalPairingSupported;

const hostedPersonalPairing = React.useMemo(
() =>
Expand All @@ -88,16 +92,18 @@ export function ConduitActionsProvider({ children }: React.PropsWithChildren) {
state.stationPhase,
],
);
const personalCompartmentId =
Platform.OS === "ios"
? hostedPersonalPairing.ready
? hostedPersonalPairing.hostedPersonalCompartmentId
: null
: (hostedPersonalPairing.hostedPersonalCompartmentId ??
androidPersonalCompartmentId ??
null);
const personalCompartmentId = hostedPairingOnly
? hostedPersonalPairing.ready
? hostedPersonalPairing.hostedPersonalCompartmentId
: null
: (hostedPersonalPairing.hostedPersonalCompartmentId ??
localPersonalCompartmentId ??
null);
const isPersonalPairingPreparing =
Platform.OS === "ios" && hostedPersonalPairing.preparing;
(hostedPairingOnly && hostedPersonalPairing.preparing) ||
(localPersonalPairingSupported &&
localPersonalCompartmentId === undefined &&
localPersonalCompartmentIdQuery.isLoading);
const personalPairingWrapperBaseUrl =
state.session?.personalPairingWrapperBaseUrl ?? null;

Expand Down
16 changes: 15 additions & 1 deletion src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import {
} from "@/src/constants";
import { loadCachedAlias } from "@/src/hosted/aliasCache";
import { PersonalCompartmentId } from "@/src/hosted/contracts";
import { loadAndroidPersonalCompartmentId } from "@/src/personalCompartmentId";
import {
isLocalPersonalPairingSupported,
loadAndroidPersonalCompartmentId,
loadLocalPersonalCompartmentId,
} from "@/src/personalCompartmentId";

const PermissionsStatusSchema = z.enum([
"GRANTED",
Expand Down Expand Up @@ -66,6 +70,16 @@ export const useAndroidPersonalCompartmentId =
queryFn: async () => loadAndroidPersonalCompartmentId(),
});

export const useLocalPersonalCompartmentId =
(): UseQueryResult<PersonalCompartmentId | null> =>
useQuery({
queryKey: [QUERYKEY_ANDROID_PERSONAL_COMPARTMENT_ID],
staleTime: Infinity,
gcTime: Infinity,
enabled: isLocalPersonalPairingSupported(),
queryFn: async () => loadLocalPersonalCompartmentId(),
});

export const useConduitName = (): UseQueryResult<string> => {
return useQuery({
queryKey: [QUERYKEY_CONDUIT_NAME],
Expand Down
34 changes: 34 additions & 0 deletions src/personalCompartmentId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,32 @@ import {
PersonalCompartmentIdSchema,
} from "@/src/hosted/contracts";

interface ExtendedPlatformConstants {
interfaceIdiom?: string;
isMacCatalyst?: boolean;
}

export async function loadAndroidPersonalCompartmentId(): Promise<PersonalCompartmentId | null> {
if (Platform.OS !== "android") {
return null;
}

return loadOrCreatePersistedPersonalCompartmentId();
}

export async function loadLocalPersonalCompartmentId(): Promise<PersonalCompartmentId | null> {
if (!isLocalPersonalPairingSupported()) {
return null;
}

return loadOrCreatePersistedPersonalCompartmentId();
}

export function isLocalPersonalPairingSupported(): boolean {
return Platform.OS === "android" || isMacCatalyst();
}

async function loadOrCreatePersistedPersonalCompartmentId(): Promise<PersonalCompartmentId | null> {
const storedPersonalCompartmentId = parsePersonalCompartmentId(
await SecureStore.getItemAsync(
SECURESTORE_ANDROID_PERSONAL_COMPARTMENT_ID_KEY,
Expand Down Expand Up @@ -94,3 +115,16 @@ async function derivePersonalCompartmentId(): Promise<PersonalCompartmentId | nu
base64nopad.encode(inproxyKeyPair.publicKey),
);
}

function isMacCatalyst(): boolean {
if (Platform.OS !== "ios") {
return false;
}

const constants = Platform.constants as ExtendedPlatformConstants;
return (
constants.isMacCatalyst === true ||
constants.interfaceIdiom === "mac" ||
constants.interfaceIdiom === "macos"
);
}