authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 16:58:15-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 16:58:15-07:00
log28e7c9ae17affc31e028a06cf33911fabe84d2cd
tree61917af2f664fcae74b627da8ad8993ef1a7f175
parenta9460bb772a8e25d39213ae62e73934152d44495
signature Signed by PGP key B5690EEEBB952194

[codex] Structure mobile waitlist enrollment failures (#3446)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 103 insertions(+), 6 deletions(-)

apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx+9-6
...@@ -4,6 +4,7 @@ import { useState } from "react";...@@ -4,6 +4,7 @@ import { useState } from "react";
44
5import { MOBILE_TYPOGRAPHY } from "../../lib/typography";5import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
6import { useThemeColor } from "../../lib/useThemeColor";6import { useThemeColor } from "../../lib/useThemeColor";
7import { CloudWaitlistJoinRejectedError, joinCloudWaitlist } from "./cloudWaitlistJoin";
78
8export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) {9export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) {
9 const { errors, fetchStatus, waitlist } = useWaitlist();10 const { errors, fetchStatus, waitlist } = useWaitlist();
...@@ -21,12 +22,14 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }...@@ -21,12 +22,14 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }
2122
22 setRequestError(null);23 setRequestError(null);
23 try {24 try {
24 const { error } = await waitlist.join({ emailAddress: normalizedEmailAddress });25 await joinCloudWaitlist(waitlist, normalizedEmailAddress);
25 if (error) {26 } catch (error) {
26 setRequestError("Could not join the waitlist. Check your email address and try again.");27 console.error(error);
27 }28 setRequestError(
28 } catch {29 error instanceof CloudWaitlistJoinRejectedError
29 setRequestError("Could not join the waitlist. Check your connection and try again.");30 ? "Could not join the waitlist. Check your email address and try again."
31 : "Could not join the waitlist. Check your connection and try again.",
32 );
30 }33 }
31 };34 };
3235
apps/mobile/src/features/cloud/cloudWaitlistJoin.test.ts created+48
...@@ -0,0 +1,48 @@
1import { describe, expect, it, vi } from "vite-plus/test";
2
3import {
4 CloudWaitlistJoinRejectedError,
5 CloudWaitlistJoinRequestError,
6 joinCloudWaitlist,
7} from "./cloudWaitlistJoin";
8
9describe("joinCloudWaitlist", () => {
10 it("submits the provided email address", async () => {
11 const join = vi.fn().mockResolvedValue({ error: null });
12
13 await joinCloudWaitlist({ join }, "person@example.com");
14
15 expect(join).toHaveBeenCalledExactlyOnceWith({ emailAddress: "person@example.com" });
16 });
17
18 it("preserves Clerk rejection details without exposing the email address", async () => {
19 const cause = Object.assign(new Error("The enrollment was rejected."), {
20 code: "form_identifier_invalid",
21 });
22 const join = vi.fn().mockResolvedValue({ error: cause });
23
24 const failure = await joinCloudWaitlist({ join }, "secret@example.com").catch(
25 (error: unknown) => error,
26 );
27
28 expect(failure).toBeInstanceOf(CloudWaitlistJoinRejectedError);
29 expect(failure).toMatchObject({
30 code: "form_identifier_invalid",
31 cause,
32 });
33 expect(String(failure)).not.toContain("secret@example.com");
34 });
35
36 it("distinguishes request failures from rejected enrollments", async () => {
37 const cause = new Error("network unavailable");
38 const join = vi.fn().mockRejectedValue(cause);
39
40 const failure = await joinCloudWaitlist({ join }, "person@example.com").catch(
41 (error: unknown) => error,
42 );
43
44 expect(failure).toBeInstanceOf(CloudWaitlistJoinRequestError);
45 expect(failure).toMatchObject({ cause });
46 expect(failure).not.toBeInstanceOf(CloudWaitlistJoinRejectedError);
47 });
48});
apps/mobile/src/features/cloud/cloudWaitlistJoin.ts created+46
...@@ -0,0 +1,46 @@
1import * as Schema from "effect/Schema";
2
3interface CloudWaitlistJoiner {
4 readonly join: (input: { emailAddress: string }) => Promise<{
5 readonly error: { readonly code: string } | null;
6 }>;
7}
8
9export class CloudWaitlistJoinRejectedError extends Schema.TaggedErrorClass<CloudWaitlistJoinRejectedError>()(
10 "CloudWaitlistJoinRejectedError",
11 {
12 code: Schema.String,
13 cause: Schema.Defect(),
14 },
15) {
16 override get message(): string {
17 return `Cloud waitlist enrollment was rejected with code "${this.code}".`;
18 }
19}
20
21export class CloudWaitlistJoinRequestError extends Schema.TaggedErrorClass<CloudWaitlistJoinRequestError>()(
22 "CloudWaitlistJoinRequestError",
23 {
24 cause: Schema.Defect(),
25 },
26) {
27 override get message(): string {
28 return "Cloud waitlist enrollment request failed.";
29 }
30}
31
32export async function joinCloudWaitlist(
33 waitlist: CloudWaitlistJoiner,
34 emailAddress: string,
35): Promise<void> {
36 const result = await waitlist.join({ emailAddress }).catch((cause) => {
37 throw new CloudWaitlistJoinRequestError({ cause });
38 });
39
40 if (result.error) {
41 throw new CloudWaitlistJoinRejectedError({
42 code: result.error.code,
43 cause: result.error,
44 });
45 }
46}