diff --git a/app/(onboarding)/tipping/onboarding/_components/join-group-form.tsx b/app/(onboarding)/tipping/onboarding/_components/join-group-form.tsx index 637ff7d..e8a98d1 100644 --- a/app/(onboarding)/tipping/onboarding/_components/join-group-form.tsx +++ b/app/(onboarding)/tipping/onboarding/_components/join-group-form.tsx @@ -1,9 +1,23 @@ 'use client' -import { InputGroup, InputGroupAddon, InputGroupInput } from '@ui/input-group' import { zodResolver } from '@hookform/resolvers/zod' +import { isCuid } from '@paralleldrive/cuid2' +import { Button } from '@ui/button' +import { InputGroup, InputGroupAddon, InputGroupInput } from '@ui/input-group' +import { + LinkIcon, + LucideAlertCircle, + LucideCheck, + LucideChevronRight, + LucideSearch, +} from 'lucide-react' +import posthog from 'posthog-js' +import React from 'react' import { Controller, useForm } from 'react-hook-form' import { z } from 'zod' +import { findGroup } from '@/actions/join-group' +import Alert from '@/components/alert' +import { IconFromName } from '@/components/icon-from-name' import { Card, CardContent, @@ -19,23 +33,9 @@ import { FieldLabel, FieldSet, } from '@/components/ui/field' -import { Button } from '@ui/button' -import { - LinkIcon, - LucideAlertCircle, - LucideCheck, - LucideChevronRight, - LucideSearch, -} from 'lucide-react' -import React from 'react' import { Spinner } from '@/components/ui/spinner' -import { isCuid } from '@paralleldrive/cuid2' -import { findGroup } from '@/actions/join-group' -import Alert from '@/components/alert' -import { IconFromName } from '@/components/icon-from-name' -import { useOnboarding } from '../_lib/onboarding-context' -import posthog from 'posthog-js' import { AnalyticsEvent } from '@/lib/posthog/events' +import { useOnboarding } from '../_lib/onboarding-context' export type JoinGroupData = NonNullable< Awaited>['data'] @@ -57,13 +57,16 @@ export default function JoinGroupForm() { { message: 'Invalid invite link' }, ), }) + + const initialUrl = state.joinGroupScreenData?.id + ? `https://gridtipapp.com/join/${state.joinGroupScreenData.id}` + : state.pendingInviteUrl || '' + const form = useForm({ mode: 'onBlur', resolver: zodResolver(schema), defaultValues: { - url: state.joinGroupScreenData?.id - ? `https://gridtipapp.com/join/${state.joinGroupScreenData.id}` - : '', + url: initialUrl, }, }) @@ -92,6 +95,19 @@ export default function JoinGroupForm() { const [isPending, startTransition] = React.useTransition() + // Auto-fetch group details if we have a pending invite URL from the join page + const hasFetchedPendingInvite = React.useRef(false) + React.useEffect(() => { + if ( + state.pendingInviteUrl && + !state.joinGroupScreenData && + !hasFetchedPendingInvite.current + ) { + hasFetchedPendingInvite.current = true + fetchGroupFromUrl(state.pendingInviteUrl) + } + }, [state.pendingInviteUrl]) // eslint-disable-line react-hooks/exhaustive-deps + return (
@@ -190,9 +206,12 @@ export default function JoinGroupForm() { function onSubmit(data: FormSchema) { setGroupState(undefined) + fetchGroupFromUrl(data.url) + } + function fetchGroupFromUrl(url: string) { startTransition(async () => { - const groupId = getId(data.url) + const groupId = getId(url) if (!groupId) { setGroupState({ state: 'error', diff --git a/app/(onboarding)/tipping/onboarding/_lib/onboarding-context.tsx b/app/(onboarding)/tipping/onboarding/_lib/onboarding-context.tsx index 4bc90af..4e1a295 100644 --- a/app/(onboarding)/tipping/onboarding/_lib/onboarding-context.tsx +++ b/app/(onboarding)/tipping/onboarding/_lib/onboarding-context.tsx @@ -5,20 +5,22 @@ import { ReactNode, useCallback, useContext, + useEffect, useState, } from 'react' -import { GroupAction } from '../_components/onboarding-client' -import { JoinGroupData } from '../_components/join-group-form' -import { ProfileState } from '../_components/screens/profile-screen' -import type { DalUser } from '@/lib/dal' +import { toast } from 'sonner' import { - joinOrCreateGroupAndUpdateImage, - joinGlobalGroupIfDesiredAndUpdateImage, completeProfileOnboardingAction, + joinGlobalGroupIfDesiredAndUpdateImage, + joinOrCreateGroupAndUpdateImage, type Log, } from '@/actions/complete-onboarding' -import { toast } from 'sonner' +import type { DalUser } from '@/lib/dal' +import { consumePendingInviteUrlFromLocalStorage } from '@/lib/utils/pending-invite' import { OnboardingCreateGroupFormData } from '../_components/create-group-form' +import { JoinGroupData } from '../_components/join-group-form' +import { GroupAction } from '../_components/onboarding-client' +import { ProfileState } from '../_components/screens/profile-screen' type ComponentKey = | 'welcome-initial' @@ -35,6 +37,8 @@ export type OnboardingState = { createGroupScreenData?: OnboardingCreateGroupFormData joinGroupScreenData?: JoinGroupData + pendingInviteUrl?: string + globalGroupScreenData?: { isJoin: boolean } profileDefaultData?: ProfileState @@ -72,6 +76,18 @@ export function OnboardingProvider({ profileCreateGroupData: defaultProfileData, }) + useEffect(() => { + const url = consumePendingInviteUrlFromLocalStorage() + if (url) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setState((prev) => ({ + ...prev, + pendingInviteUrl: url, + welcomeScreenSelectedGroupStep: 'join', + })) + } + }, []) + const goToScreen = useCallback((component: ComponentKey) => { setState((prevState) => ({ ...prevState, diff --git a/app/(standalone)/join/[groupId]/_components/invite-buttons.tsx b/app/(standalone)/join/[groupId]/_components/invite-buttons.tsx index fa9d266..28fad74 100644 --- a/app/(standalone)/join/[groupId]/_components/invite-buttons.tsx +++ b/app/(standalone)/join/[groupId]/_components/invite-buttons.tsx @@ -1,17 +1,28 @@ 'use client' -import { Button } from '@/components/ui/button' import Link from 'next/link' -import { Path } from '@/lib/utils/path' import posthog from 'posthog-js' +import { Button } from '@/components/ui/button' import { AnalyticsEvent } from '@/lib/posthog/events' +import { Path } from '@/lib/utils/path' +import { savePendingInviteUrlToLocalStorage } from '@/lib/utils/pending-invite' -export function InviteButtons({ loginHref }: { loginHref: string }) { +export function InviteButtons({ + loginHref, + groupId, +}: { + loginHref: string + groupId: string +}) { return (
diff --git a/app/(standalone)/join/[groupId]/page.tsx b/app/(standalone)/join/[groupId]/page.tsx index c63e9ef..2c4b4fd 100644 --- a/app/(standalone)/join/[groupId]/page.tsx +++ b/app/(standalone)/join/[groupId]/page.tsx @@ -143,7 +143,7 @@ export default async function JoinGroup({ const href = withQuery(getAuthLinkWithOrigin(QueryOrigin.Join), { redirect: `/join/${groupId}`, }) - return + return } return } diff --git a/lib/utils/pending-invite.ts b/lib/utils/pending-invite.ts new file mode 100644 index 0000000..4045021 --- /dev/null +++ b/lib/utils/pending-invite.ts @@ -0,0 +1,29 @@ +const STORAGE_KEY = 'gridtip:pendingInviteUrl' + +export function savePendingInviteUrlToLocalStorage(joinUrl: string) { + try { + localStorage.setItem(STORAGE_KEY, joinUrl) + } catch { + // localStorage unavailable or full — silently ignore + } +} + +export function consumePendingInviteUrlFromLocalStorage(): string | null { + try { + const url = localStorage.getItem(STORAGE_KEY) + if (url) { + localStorage.removeItem(STORAGE_KEY) + } + return url + } catch { + return null + } +} + +export function clearPendingInviteUrlFromLocalStorage() { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // silently ignore + } +}