Unified · Split
| @@ -4,6 +4,7 @@ import { useState } from "react"; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; |
| 6 | 6 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 7 | +import { CloudWaitlistJoinRejectedError, joinCloudWaitlist } from "./cloudWaitlistJoin"; | |
| 7 | 8 | |
| 8 | 9 | export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) { |
| 9 | 10 | const { errors, fetchStatus, waitlist } = useWaitlist(); |
| @@ -21,12 +22,14 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } · 11 unmodified lines | ||
| 21 | 22 | |
| 22 | 23 | setRequestError(null); |
| 23 | 24 | try { |
| 24 | − const { error } = await waitlist.join({ emailAddress: normalizedEmailAddress }); | |
| 25 | − if (error) { | |
| 26 | − setRequestError("Could not join the waitlist. Check your email address and try again."); | |
| 27 | − } | |
| 28 | − } catch { | |
| 29 | − setRequestError("Could not join the waitlist. Check your connection and try again."); | |
| 25 | + await joinCloudWaitlist(waitlist, normalizedEmailAddress); | |
| 26 | + } catch (error) { | |
| 27 | + console.error(error); | |
| 28 | + setRequestError( | |
| 29 | + error instanceof CloudWaitlistJoinRejectedError | |
| 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 | }; |
| 32 | 35 | |
| @@ -0,0 +1,48 @@ | ||
| 1 | +import { describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + CloudWaitlistJoinRejectedError, | |
| 5 | + CloudWaitlistJoinRequestError, | |
| 6 | + joinCloudWaitlist, | |
| 7 | +} from "./cloudWaitlistJoin"; | |
| 8 | + | |
| 9 | +describe("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 | +}); | |
| @@ -0,0 +1,46 @@ | ||
| 1 | +import * as Schema from "effect/Schema"; | |
| 2 | + | |
| 3 | +interface CloudWaitlistJoiner { | |
| 4 | + readonly join: (input: { emailAddress: string }) => Promise<{ | |
| 5 | + readonly error: { readonly code: string } | null; | |
| 6 | + }>; | |
| 7 | +} | |
| 8 | + | |
| 9 | +export 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 | + | |
| 21 | +export 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 | + | |
| 32 | +export 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 | +} | |