Skip to content

Commit 60a79e4

Browse files
vincangerMartinsos
andauthored
Remove hasPaid from user entity and rely only on subscriptionStatus for user privileges (wasp-lang#96)
* fix userStripeId race condition * small fixes * fix operations * Update app/src/server/queries.ts Co-authored-by: Martin Šošić <[email protected]> * Update app/src/server/queries.ts Co-authored-by: Martin Šošić <[email protected]> * fix typos and remove console logs * Update PricingPage.tsx * add pricing page tests * add copying .env.client.example --------- Co-authored-by: Martin Šošić <[email protected]>
1 parent 2d94e28 commit 60a79e4

File tree

19 files changed

+216
-114
lines changed

19 files changed

+216
-114
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- name: Set required wasp app env vars to mock values
4040
run: |
4141
cd app
42-
cp .env.server.example .env.server
42+
cp .env.server.example .env.server && cp .env.client.example .env.client
4343
4444
- name: Cache global node modules
4545
uses: actions/cache@v4

app/.env.client.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# All client-side env vars must start with REACT_APP_ https://wasp-lang.dev/docs/project/env-vars
22

33
# Find your test url at https://dashboard.stripe.com/test/settings/billing/portal
4-
REACT_APP_STRIPE_CUSTOMER_PORTAL=
4+
REACT_APP_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/...

app/main.wasp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ entity User {=psl
8787
isAdmin Boolean @default(false)
8888
stripeId String?
8989
checkoutSessionId String?
90-
hasPaid Boolean @default(false)
9190
subscriptionTier String?
9291
subscriptionStatus String?
9392
sendEmail Boolean @default(false)

app/src/client/admin/components/CheckboxOne.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ const CheckboxOne = () => {
66

77
return (
88
<div>
9-
<label htmlFor='checkboxLabelFour' className='flex cursor-pointer select-none items-center'>
10-
<div className='relative'>
9+
<label
10+
htmlFor="checkboxLabelOne"
11+
className="flex cursor-pointer select-none items-center"
12+
>
13+
<div className="relative">
1114
<input
12-
type='checkbox'
13-
id='checkboxLabelFour'
14-
className='sr-only'
15+
type="checkbox"
16+
id="checkboxLabelOne"
17+
className="sr-only"
1518
onChange={() => {
1619
setIsChecked(!isChecked);
1720
}}

app/src/client/admin/components/CheckboxTwo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const CheckboxTwo = () => {
66
return (
77
<div>
88
<label htmlFor='checkboxLabelTwo' className='flex cursor-pointer text-sm text-gray-700 select-none items-center'>
9-
hasPaid:
9+
enabled:
1010
<div className='relative'>
1111
<input
1212
type='checkbox'

app/src/client/admin/components/SwitcherOne.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useState } from 'react';
33
import { cn } from '../../../shared/utils';
44

55
const SwitcherOne = ({ user, updateUserById }: { user?: Partial<User>; updateUserById?: any }) => {
6-
const [enabled, setEnabled] = useState<boolean>(user?.hasPaid || false);
6+
const [enabled, setEnabled] = useState<boolean>(user?.isAdmin || false);
77

88
return (
99
<div className='relative'>
@@ -15,7 +15,7 @@ const SwitcherOne = ({ user, updateUserById }: { user?: Partial<User>; updateUse
1515
className='sr-only'
1616
onChange={() => {
1717
setEnabled(!enabled);
18-
updateUserById && updateUserById({ id: user?.id, data: { hasPaid: !enabled } });
18+
updateUserById && updateUserById({ id: user?.id, data: { isAdmin: !enabled } });
1919
}}
2020
/>
2121
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>

app/src/client/admin/components/UsersTable.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@ import { useState, useEffect } from 'react';
33
import SwitcherOne from './SwitcherOne';
44
import Loader from '../common/Loader';
55
import DropdownEditDelete from './DropdownEditDelete';
6-
7-
type StatusOptions = 'past_due' | 'canceled' | 'active' | 'deleted';
6+
import { type SubscriptionStatusOptions } from '../../../shared/types';
87

98
const UsersTable = () => {
109
const [skip, setskip] = useState(0);
1110
const [page, setPage] = useState(1);
1211
const [email, setEmail] = useState<string | undefined>(undefined);
13-
const [statusOptions, setStatusOptions] = useState<StatusOptions[]>([]);
14-
const [hasPaidFilter, setHasPaidFilter] = useState<boolean | undefined>(undefined);
12+
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
13+
const [statusOptions, setStatusOptions] = useState<SubscriptionStatusOptions[]>([]);
1514
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
1615
skip,
17-
hasPaidFilter: hasPaidFilter,
1816
emailContains: email,
17+
isAdmin: isAdminFilter,
1918
subscriptionStatus: statusOptions?.length > 0 ? statusOptions : undefined,
2019
});
2120

@@ -57,7 +56,7 @@ const UsersTable = () => {
5756
key={opt}
5857
className='z-30 flex items-center my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
5958
>
60-
{opt}
59+
{opt ? opt : 'has not subscribed'}
6160
<span
6261
onClick={(e) => {
6362
e.stopPropagation();
@@ -92,11 +91,12 @@ const UsersTable = () => {
9291
</div>
9392
<select
9493
onChange={(e) => {
94+
const targetValue = e.target.value === '' ? null : e.target.value;
9595
setStatusOptions((prevValue) => {
96-
if (prevValue?.includes(e.target.value as StatusOptions)) {
97-
return prevValue?.filter((val) => val !== e.target.value);
96+
if (prevValue?.includes(targetValue as SubscriptionStatusOptions)) {
97+
return prevValue?.filter((val) => val !== targetValue);
9898
} else if (!!prevValue) {
99-
return [...prevValue, e.target.value as StatusOptions];
99+
return [...prevValue, targetValue as SubscriptionStatusOptions];
100100
} else {
101101
return prevValue;
102102
}
@@ -107,9 +107,9 @@ const UsersTable = () => {
107107
className='absolute top-0 left-0 z-20 h-full w-full bg-white opacity-0'
108108
>
109109
<option value=''>Select filters</option>
110-
{['past_due', 'canceled', 'active'].map((status) => {
111-
if (!statusOptions.includes(status as StatusOptions)) {
112-
return <option value={status}>{status}</option>;
110+
{['past_due', 'canceled', 'active', 'deleted', null].map((status) => {
111+
if (!statusOptions.includes(status as SubscriptionStatusOptions)) {
112+
return <option value={status || ''}>{status ? status : 'has not subscribed'}</option>;
113113
}
114114
})}
115115
</select>
@@ -127,16 +127,16 @@ const UsersTable = () => {
127127
</span>
128128
</div>
129129
<div className='flex items-center gap-2'>
130-
<label htmlFor='hasPaid-filter' className='block text-sm ml-2 text-gray-700 dark:text-white'>
131-
hasPaid:
130+
<label htmlFor='isAdmin-filter' className='block text-sm ml-2 text-gray-700 dark:text-white'>
131+
isAdmin:
132132
</label>
133133
<select
134-
name='hasPaid-filter'
134+
name='isAdmin-filter'
135135
onChange={(e) => {
136136
if (e.target.value === 'both') {
137-
setHasPaidFilter(undefined);
137+
setIsAdminFilter(undefined);
138138
} else {
139-
setHasPaidFilter(e.target.value === 'true');
139+
setIsAdminFilter(e.target.value === 'true');
140140
}
141141
}}
142142
className='relative z-20 w-full appearance-none rounded border border-stroke bg-white p-2 pl-4 pr-8 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'
@@ -180,7 +180,7 @@ const UsersTable = () => {
180180
<p className='font-medium'>Stripe ID</p>
181181
</div>
182182
<div className='col-span-1 flex items-center'>
183-
<p className='font-medium'>Has Paid</p>
183+
<p className='font-medium'>Is Admin</p>
184184
</div>
185185
<div className='col-span-1 flex items-center'>
186186
<p className='font-medium'></p>

app/src/client/app/AccountPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function AccountPage({ user }: { user: User }) {
2727
)}
2828
<div className='py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6'>
2929
<dt className='text-sm font-medium text-gray-500 dark:text-white'>Your Plan</dt>
30-
{user.hasPaid ? (
30+
{!!user.subscriptionStatus ? (
3131
<>
3232
{user.subscriptionStatus !== 'past_due' ? (
3333
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>

app/src/client/app/PricingPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const PricingPage = () => {
129129
))}
130130
</ul>
131131
</div>
132-
{!!user && user.hasPaid ? (
132+
{!!user && !!user.subscriptionStatus ? (
133133
<button
134134
onClick={handleCustomerPortalClick}
135135
aria-describedby='manage-subscription'
@@ -153,7 +153,7 @@ const PricingPage = () => {
153153
'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400': !tier.bestDeal,
154154
},
155155
{
156-
'cursor-wait': isStripePaymentLoading === tier.id,
156+
'opacity-50 cursor-wait cursor-not-allowed': isStripePaymentLoading === tier.id,
157157
},
158158
'mt-8 block rounded-md py-2 px-3 text-center text-sm dark:text-white font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-yellow-400'
159159
)}

app/src/server/actions.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,14 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSched
108108
}));
109109

110110
try {
111-
if (!context.user.hasPaid && !context.user.credits) {
111+
// check if openai is initialized correctly with the API key
112+
if (openai instanceof Error) {
113+
throw openai;
114+
}
115+
116+
if (!context.user.subscriptionStatus && !context.user.credits) {
112117
throw new HttpError(402, 'User has not paid or is out of credits');
113-
} else if (context.user.credits && !context.user.hasPaid) {
118+
} else if (context.user.credits && !context.user.subscriptionStatus) {
114119
console.log('decrementing credits');
115120
await context.entities.User.update({
116121
where: { id: context.user.id },
@@ -122,13 +127,8 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSched
122127
});
123128
}
124129

125-
// check if openai is initialized correctly with the API key
126-
if (openai instanceof Error) {
127-
throw openai;
128-
}
129-
130130
const completion = await openai.chat.completions.create({
131-
model: 'gpt-3.5-turbo',
131+
model: 'gpt-3.5-turbo', // you can use any model here, e.g. 'gpt-3.5-turbo', 'gpt-4', etc.
132132
messages: [
133133
{
134134
role: 'system',
@@ -222,7 +222,7 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSched
222222

223223
return JSON.parse(gptArgs);
224224
} catch (error: any) {
225-
if (!context.user.hasPaid && error?.statusCode != 402) {
225+
if (!context.user.subscriptionStatus && error?.statusCode != 402) {
226226
await context.entities.User.update({
227227
where: { id: context.user.id },
228228
data: {

0 commit comments

Comments
 (0)