Skip to content

Commit c31cc53

Browse files
authored
Merge pull request #341 from Merit-Systems/json/welcome
New User Flow - Sign TOS + PP, Get Issued Free Credits
2 parents c9c474f + b482568 commit c31cc53

37 files changed

Lines changed: 1387 additions & 84 deletions

File tree

echo-control/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@prisma/client": "^6.13.0",
4444
"@prisma/nextjs-monorepo-workaround-plugin": "^6.12.0",
4545
"@radix-ui/react-accordion": "^1.2.12",
46+
"@radix-ui/react-alert-dialog": "^1.1.15",
4647
"@radix-ui/react-avatar": "^1.1.10",
4748
"@radix-ui/react-dialog": "^1.1.14",
4849
"@radix-ui/react-dropdown-menu": "^2.1.15",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- AlterTable
2+
ALTER TABLE "users" ADD COLUMN "latestFreeCreditsVersion" DECIMAL(65,30),
3+
ADD COLUMN "latestPrivacyVersion" DECIMAL(65,30),
4+
ADD COLUMN "latestTosVersion" DECIMAL(65,30);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- CreateEnum
2+
CREATE TYPE "EnumPaymentSource" AS ENUM ('stripe', 'admin', 'signUpGift');
3+
4+
-- AlterTable
5+
ALTER TABLE "payments" ADD COLUMN "source" "EnumPaymentSource" NOT NULL DEFAULT 'stripe';

echo-control/prisma/schema.prisma

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ model User {
3535
appSessions AppSession[]
3636
Payout Payout[]
3737
githubLink GithubLink?
38+
latestTosVersion Decimal?
39+
latestPrivacyVersion Decimal?
40+
latestFreeCreditsVersion Decimal?
3841
3942
@@map("users")
4043
}
@@ -200,6 +203,7 @@ model Payment {
200203
amount Decimal @db.Decimal(65, 14)
201204
currency String @default("usd")
202205
status String
206+
source EnumPaymentSource @default(stripe)
203207
description String?
204208
isArchived Boolean @default(false)
205209
archivedAt DateTime? @db.Timestamptz(6)
@@ -273,6 +277,12 @@ enum GithubType {
273277
repo
274278
}
275279

280+
enum EnumPaymentSource {
281+
stripe
282+
admin
283+
signUpGift
284+
}
285+
276286
model Transaction {
277287
id String @id @default(uuid()) @db.Uuid
278288
transactionMetadataId String? @db.Uuid
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use client';
2+
3+
import { api } from '@/trpc/client';
4+
import {
5+
AlertDialog,
6+
AlertDialogContent,
7+
AlertDialogHeader,
8+
AlertDialogTitle,
9+
AlertDialogDescription,
10+
AlertDialogFooter,
11+
AlertDialogAction,
12+
} from '@/components/ui/alert-dialog';
13+
import Link from 'next/link';
14+
import { Check, Loader2 } from 'lucide-react';
15+
import { toast } from 'sonner';
16+
import { useSession } from 'next-auth/react';
17+
18+
export default function TermsAgreement() {
19+
const session = useSession();
20+
21+
const { data: needsTerms } = api.user.termsAgreement.needs.terms.useQuery(
22+
undefined,
23+
{
24+
enabled: !!session.data?.user,
25+
}
26+
);
27+
28+
const utils = api.useUtils();
29+
30+
const {
31+
mutate: acceptTerms,
32+
isPending: isAcceptingTerms,
33+
isSuccess: isAcceptedTerms,
34+
} = api.user.termsAgreement.accept.terms.useMutation({
35+
onSuccess: () => {
36+
toast.success('Terms of Service accepted');
37+
utils.user.termsAgreement.needs.terms.invalidate();
38+
},
39+
});
40+
41+
return (
42+
<AlertDialog open={needsTerms?.needs}>
43+
<AlertDialogContent>
44+
<AlertDialogHeader>
45+
<AlertDialogTitle>Terms of Service</AlertDialogTitle>
46+
<AlertDialogDescription>
47+
{needsTerms?.currentVersion ? (
48+
'Our Terms of Service have changed. Please confirm you accept the latest version.'
49+
) : (
50+
<>
51+
Please accept our{' '}
52+
<Link href="/terms" target="_blank" className="underline">
53+
Terms of Service
54+
</Link>{' '}
55+
to continue.
56+
</>
57+
)}
58+
</AlertDialogDescription>
59+
</AlertDialogHeader>
60+
<AlertDialogFooter>
61+
<AlertDialogAction
62+
onClick={() => acceptTerms()}
63+
className="flex-1"
64+
disabled={isAcceptingTerms || isAcceptedTerms}
65+
>
66+
{isAcceptingTerms ? (
67+
<Loader2 className="size-4 animate-spin" />
68+
) : isAcceptedTerms ? (
69+
<Check className="size-4" />
70+
) : (
71+
'Accept Terms'
72+
)}
73+
</AlertDialogAction>
74+
</AlertDialogFooter>
75+
</AlertDialogContent>
76+
</AlertDialog>
77+
);
78+
}

echo-control/src/app/(app)/app/[id]/settings/monetization/_components/markup/input.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const MarkupInput = () => {
1818
<MarkupInputComponent
1919
markup={field.value}
2020
onMarkupChange={value => {
21-
console.log(value);
2221
field.onChange(value);
2322
}}
2423
/>

echo-control/src/app/(app)/layout.tsx

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { UserDropdown } from './_components/layout/header/user-dropdown';
1010
import { LogoContainer } from './_components/layout/logo';
1111

1212
import { Route } from 'next';
13+
import TermsAgreement from '@/app/(app)/_components/terms';
1314

1415
export const dynamic = 'force-dynamic';
1516

@@ -18,30 +19,34 @@ export default async function AppLayout({
1819
breadcrumbs,
1920
}: LayoutProps<'/'>) {
2021
return (
21-
<div className="min-h-screen flex flex-col relative">
22-
<LogoContainer>
23-
<Link href="/dashboard">
24-
<Logo className="size-auto h-full aspect-square" />
25-
</Link>
26-
</LogoContainer>
27-
<header className="w-full flex flex-col pt-4 justify-center">
28-
<div className="flex items-center justify-between w-full px-2 md:px-6 pb-0 md:pb-0 h-10">
29-
<div className="pl-10 md:pl-12 flex items-center gap-2 md:gap-3">
30-
{breadcrumbs}
22+
<>
23+
{/* <FreeTier /> */}
24+
<TermsAgreement />
25+
<div className="min-h-screen flex flex-col relative">
26+
<LogoContainer>
27+
<Link href="/dashboard">
28+
<Logo className="size-auto h-full aspect-square" />
29+
</Link>
30+
</LogoContainer>
31+
<header className="w-full flex flex-col pt-4 justify-center">
32+
<div className="flex items-center justify-between w-full px-2 md:px-6 pb-0 md:pb-0 h-10">
33+
<div className="pl-10 md:pl-12 flex items-center gap-2 md:gap-3">
34+
{breadcrumbs}
35+
</div>
36+
<div className="flex items-center gap-1 md:gap-2">
37+
<BalanceButton />
38+
<Link href={'/docs' as Route<'/docs'>}>
39+
<Button variant="outline" size="navbar">
40+
<Book className="size-4" />
41+
<span className="hidden md:block">Docs</span>
42+
</Button>
43+
</Link>
44+
<UserDropdown />
45+
</div>
3146
</div>
32-
<div className="flex items-center gap-1 md:gap-2">
33-
<BalanceButton />
34-
<Link href={'/docs' as Route<'/docs'>}>
35-
<Button variant="outline" size="navbar">
36-
<Book className="size-4" />
37-
<span className="hidden md:block">Docs</span>
38-
</Button>
39-
</Link>
40-
<UserDropdown />
41-
</div>
42-
</div>
43-
</header>
44-
<div className="bg-background flex-1">{children}</div>
45-
</div>
47+
</header>
48+
<div className="bg-background flex-1">{children}</div>
49+
</div>
50+
</>
4651
);
4752
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client';
2+
3+
import { Coupon } from '@/components/coupon';
4+
import { STATES } from '@/components/coupon/multi-state-button';
5+
import { api } from '@/trpc/client';
6+
import { toast } from 'sonner';
7+
8+
interface Props {
9+
amount: number;
10+
onSuccess: () => void;
11+
states?: STATES;
12+
subText?: React.ReactNode;
13+
}
14+
15+
export const WelcomeCoupon: React.FC<Props> = ({
16+
amount,
17+
onSuccess,
18+
states,
19+
subText,
20+
}) => {
21+
const utils = api.useUtils();
22+
23+
const {
24+
mutate: signTerms,
25+
isPending: isSigningTerms,
26+
isSuccess: isSignedTerms,
27+
} = api.user.termsAgreement.accept.terms.useMutation({
28+
onError: () => {
29+
toast.error('Failed to accept terms');
30+
utils.user.termsAgreement.needs.terms.invalidate();
31+
},
32+
});
33+
34+
const {
35+
mutate: claimCoupon,
36+
isPending: isClaimingCoupon,
37+
isSuccess: isClaimedCoupon,
38+
} = api.user.initialFreeTier.issue.useMutation({
39+
onSuccess: () => {
40+
utils.user.initialFreeTier.hasClaimed.invalidate();
41+
setTimeout(() => {
42+
toast.success('Credits claimed');
43+
onSuccess();
44+
}, 500);
45+
},
46+
onError: () => {
47+
toast.error('Failed to claim credits');
48+
},
49+
});
50+
51+
return (
52+
<Coupon
53+
value={amount}
54+
onClaim={() =>
55+
signTerms(void 0, {
56+
onSuccess: () => {
57+
claimCoupon();
58+
},
59+
})
60+
}
61+
isClaiming={isClaimingCoupon || isSigningTerms}
62+
isClaimed={isClaimedCoupon && isSignedTerms}
63+
states={states}
64+
subText={subText}
65+
/>
66+
);
67+
};

echo-control/src/app/(auth)/api/oauth/authorize/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import z from 'zod';
1111

1212
const querySchema = authorizeParamsSchema.extend({
1313
prompt: z.literal('none').optional(),
14+
new_user: z.literal('true').optional(),
1415
});
1516

1617
export const GET = createZodRoute()
@@ -79,6 +80,7 @@ export const GET = createZodRoute()
7980
'response_type',
8081
'state',
8182
'referral_code',
83+
'new_user',
8284
] as const
8385
).forEach(param => {
8486
if (query[param]) {

echo-control/src/app/(auth)/oauth/authorize/_components/buttons.tsx renamed to echo-control/src/app/(auth)/oauth/authorize/_components/existing-user/buttons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useActionState } from 'react';
44

55
import { AuthorizeParams } from '@/app/(auth)/_lib/authorize';
66

7-
import { authorize } from '../_actions/authorize';
7+
import { authorize } from '../../_actions/authorize';
88
import { Button } from '@/components/ui/button';
99

1010
interface Props {

0 commit comments

Comments
 (0)