Skip to content

Commit 4368b12

Browse files
authored
Merge pull request #390 from wasp-lang/miho-email-handling-update
2 parents 8c8093e + 66d1155 commit 4368b12

File tree

8 files changed

+112
-64
lines changed

8 files changed

+112
-64
lines changed

opensaas-sh/app_diff/main.wasp.diff

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@
8888
+ configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
8989
+ }
9090
},
91-
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
9291
onAuthFailedRedirectTo: "/login",
93-
@@ -87,11 +83,11 @@
92+
onAuthSucceededRedirectTo: "/demo-app",
93+
@@ -86,11 +82,11 @@
9494
// NOTE: "Dummy" provider is just for local development purposes.
9595
// Make sure to check the server logs for the email confirmation url (it will not be sent to an address)!
9696
// Once you are ready for production, switch to e.g. "SendGrid" or "Mailgun" providers. Check out https://docs.opensaas.sh/guides/email-sending/ .
@@ -104,7 +104,7 @@
104104
},
105105
},
106106
}
107-
@@ -207,9 +203,9 @@
107+
@@ -206,9 +202,9 @@
108108
}
109109

110110
api paymentsWebhook {

opensaas-sh/app_diff/src/auth/userSignupFields.ts.diff

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,64 @@
11
--- template/app/src/auth/userSignupFields.ts
22
+++ opensaas-sh/app/src/auth/userSignupFields.ts
3-
@@ -1,11 +1,8 @@
3+
@@ -1,8 +1,6 @@
44
import { z } from 'zod';
55
import { defineUserSignupFields } from 'wasp/auth/providers/types';
66

77
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
88
-
9-
export const getEmailUserFields = defineUserSignupFields({
10-
username: (data: any) => data.email,
11-
- isAdmin: (data: any) => adminEmails.includes(data.email),
12-
email: (data: any) => data.email,
9+
const emailDataSchema = z.object({
10+
email: z.string(),
11+
});
12+
@@ -16,10 +14,6 @@
13+
const emailData = emailDataSchema.parse(data);
14+
return emailData.email;
15+
},
16+
- isAdmin: (data) => {
17+
- const emailData = emailDataSchema.parse(data);
18+
- return adminEmails.includes(emailData.email);
19+
- },
1320
});
1421

15-
@@ -29,10 +26,6 @@
22+
const githubDataSchema = z.object({
23+
@@ -45,14 +39,6 @@
1624
const githubData = githubDataSchema.parse(data);
1725
return githubData.profile.login;
1826
},
1927
- isAdmin: (data) => {
2028
- const githubData = githubDataSchema.parse(data);
21-
- return adminEmails.includes(githubData.profile.emails[0].email);
29+
- const emailInfo = getGithubEmailInfo(githubData);
30+
- if (!emailInfo.verified) {
31+
- return false;
32+
- }
33+
- return adminEmails.includes(emailInfo.email);
2234
- },
2335
});
2436

25-
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
26-
@@ -58,10 +51,6 @@
37+
// We are using the first email from the list of emails returned by GitHub.
38+
@@ -85,13 +71,6 @@
2739
const googleData = googleDataSchema.parse(data);
2840
return googleData.profile.email;
2941
},
3042
- isAdmin: (data) => {
3143
- const googleData = googleDataSchema.parse(data);
44+
- if (!googleData.profile.email_verified) {
45+
- return false;
46+
- }
3247
- return adminEmails.includes(googleData.profile.email);
3348
- },
3449
});
3550

3651
export function getGoogleAuthConfig() {
37-
@@ -86,10 +75,6 @@
52+
@@ -121,13 +100,6 @@
3853
const discordData = discordDataSchema.parse(data);
3954
return discordData.profile.username;
4055
},
4156
- isAdmin: (data) => {
42-
- const email = discordDataSchema.parse(data).profile.email;
43-
- return !!email && adminEmails.includes(email);
57+
- const discordData = discordDataSchema.parse(data);
58+
- if (!discordData.profile.email || !discordData.profile.verified) {
59+
- return false;
60+
- }
61+
- return adminEmails.includes(discordData.profile.email);
4462
- },
4563
});
4664

opensaas-sh/app_diff/src/payment/PricingPage.tsx.diff

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,7 @@
88

99
interface PaymentPlanCard {
1010
name: string;
11-
@@ -82,7 +83,7 @@
12-
}
13-
14-
if (!customerPortalUrl) {
15-
- throw new Error(`Customer Portal does not exist for user ${user.id}`)
16-
+ throw new Error(`Customer Portal does not exist for user ${user.id}`);
17-
}
18-
19-
window.open(customerPortalUrl, '_blank');
20-
@@ -96,11 +97,18 @@
11+
@@ -105,16 +106,24 @@
2112
Pick your <span className='text-yellow-500'>pricing</span>
2213
</h2>
2314
</div>
@@ -37,11 +28,17 @@
3728
+ <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
3829
+ </p>
3930
+ </div>
31+
+
32+
{errorMessage && (
33+
<div className='mt-8 p-4 bg-red-100 text-red-600 rounded-md dark:bg-red-200 dark:text-red-800'>
34+
{errorMessage}
35+
</div>
36+
)}
4037
+
4138
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
4239
{Object.values(PaymentPlanId).map((planId) => (
4340
<div
44-
@@ -187,7 +195,7 @@
41+
@@ -201,7 +210,7 @@
4542
)}
4643
disabled={isPaymentLoading}
4744
>

template/app/main.wasp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ app OpenSaaS {
6666
// configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
6767
// }
6868
},
69-
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
7069
onAuthFailedRedirectTo: "/login",
7170
onAuthSucceededRedirectTo: "/demo-app",
7271
},

template/app/src/auth/hooks.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

template/app/src/auth/userSignupFields.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,64 @@ import { defineUserSignupFields } from 'wasp/auth/providers/types';
33

44
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
55

6+
const emailDataSchema = z.object({
7+
email: z.string(),
8+
});
9+
610
export const getEmailUserFields = defineUserSignupFields({
7-
username: (data: any) => data.email,
8-
isAdmin: (data: any) => adminEmails.includes(data.email),
9-
email: (data: any) => data.email,
11+
email: (data) => {
12+
const emailData = emailDataSchema.parse(data);
13+
return emailData.email;
14+
},
15+
username: (data) => {
16+
const emailData = emailDataSchema.parse(data);
17+
return emailData.email;
18+
},
19+
isAdmin: (data) => {
20+
const emailData = emailDataSchema.parse(data);
21+
return adminEmails.includes(emailData.email);
22+
},
1023
});
1124

1225
const githubDataSchema = z.object({
1326
profile: z.object({
14-
emails: z.array(
15-
z.object({
16-
email: z.string(),
17-
})
18-
),
27+
emails: z
28+
.array(
29+
z.object({
30+
email: z.string(),
31+
verified: z.boolean(),
32+
})
33+
)
34+
.min(1, 'You need to have an email address associated with your GitHub account to sign up.'),
1935
login: z.string(),
2036
}),
2137
});
2238

2339
export const getGitHubUserFields = defineUserSignupFields({
2440
email: (data) => {
2541
const githubData = githubDataSchema.parse(data);
26-
return githubData.profile.emails[0].email;
42+
return getGithubEmailInfo(githubData).email;
2743
},
2844
username: (data) => {
2945
const githubData = githubDataSchema.parse(data);
3046
return githubData.profile.login;
3147
},
3248
isAdmin: (data) => {
3349
const githubData = githubDataSchema.parse(data);
34-
return adminEmails.includes(githubData.profile.emails[0].email);
50+
const emailInfo = getGithubEmailInfo(githubData);
51+
if (!emailInfo.verified) {
52+
return false;
53+
}
54+
return adminEmails.includes(emailInfo.email);
3555
},
3656
});
3757

58+
// We are using the first email from the list of emails returned by GitHub.
59+
// If you want to use a different email, you can modify this function.
60+
function getGithubEmailInfo(githubData: z.infer<typeof githubDataSchema>) {
61+
return githubData.profile.emails[0];
62+
}
63+
3864
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
3965
// instead of ["user"] and access args.profile.username instead
4066
export function getGitHubAuthConfig() {
@@ -46,6 +72,7 @@ export function getGitHubAuthConfig() {
4672
const googleDataSchema = z.object({
4773
profile: z.object({
4874
email: z.string(),
75+
email_verified: z.boolean(),
4976
}),
5077
});
5178

@@ -60,6 +87,9 @@ export const getGoogleUserFields = defineUserSignupFields({
6087
},
6188
isAdmin: (data) => {
6289
const googleData = googleDataSchema.parse(data);
90+
if (!googleData.profile.email_verified) {
91+
return false;
92+
}
6393
return adminEmails.includes(googleData.profile.email);
6494
},
6595
});
@@ -74,21 +104,29 @@ const discordDataSchema = z.object({
74104
profile: z.object({
75105
username: z.string(),
76106
email: z.string().email().nullable(),
107+
verified: z.boolean().nullable(),
77108
}),
78109
});
79110

80111
export const getDiscordUserFields = defineUserSignupFields({
81112
email: (data) => {
82113
const discordData = discordDataSchema.parse(data);
114+
// Users need to have an email for payment processing.
115+
if (!discordData.profile.email) {
116+
throw new Error('You need to have an email address associated with your Discord account to sign up.');
117+
}
83118
return discordData.profile.email;
84119
},
85120
username: (data) => {
86121
const discordData = discordDataSchema.parse(data);
87122
return discordData.profile.username;
88123
},
89124
isAdmin: (data) => {
90-
const email = discordDataSchema.parse(data).profile.email;
91-
return !!email && adminEmails.includes(email);
125+
const discordData = discordDataSchema.parse(data);
126+
if (!discordData.profile.email || !discordData.profile.verified) {
127+
return false;
128+
}
129+
return adminEmails.includes(discordData.profile.email);
92130
},
93131
});
94132

template/app/src/payment/PricingPage.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ export const paymentPlanCards: Record<PaymentPlanId, PaymentPlanCard> = {
3838

3939
const PricingPage = () => {
4040
const [isPaymentLoading, setIsPaymentLoading] = useState<boolean>(false);
41-
41+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
42+
4243
const { data: user } = useAuth();
43-
const isUserSubscribed = !!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
44+
const isUserSubscribed =
45+
!!user && !!user.subscriptionStatus && user.subscriptionStatus !== SubscriptionStatus.Deleted;
4446

4547
const {
4648
data: customerPortalUrl,
@@ -65,8 +67,13 @@ const PricingPage = () => {
6567
} else {
6668
throw new Error('Error generating checkout session URL');
6769
}
68-
} catch (error) {
70+
} catch (error: unknown) {
6971
console.error(error);
72+
if (error instanceof Error) {
73+
setErrorMessage(error.message);
74+
} else {
75+
setErrorMessage('Error processing payment. Please try again later.');
76+
}
7077
setIsPaymentLoading(false); // We only set this to false here and not in the try block because we redirect to the checkout url within the same window
7178
}
7279
}
@@ -78,11 +85,13 @@ const PricingPage = () => {
7885
}
7986

8087
if (customerPortalUrlError) {
81-
console.error('Error fetching customer portal url');
88+
setErrorMessage('Error fetching Customer Portal URL');
89+
return;
8290
}
8391

8492
if (!customerPortalUrl) {
85-
throw new Error(`Customer Portal does not exist for user ${user.id}`)
93+
setErrorMessage(`Customer Portal does not exist for user ${user.id}`);
94+
return;
8695
}
8796

8897
window.open(customerPortalUrl, '_blank');
@@ -101,6 +110,11 @@ const PricingPage = () => {
101110
out below with test credit card number <br />
102111
<span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
103112
</p>
113+
{errorMessage && (
114+
<div className='mt-8 p-4 bg-red-100 text-red-600 rounded-md dark:bg-red-200 dark:text-red-800'>
115+
{errorMessage}
116+
</div>
117+
)}
104118
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
105119
{Object.values(PaymentPlanId).map((planId) => (
106120
<div

template/app/src/payment/operations.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ export const generateCheckoutSession: GenerateCheckoutSession<
2525
const userId = context.user.id;
2626
const userEmail = context.user.email;
2727
if (!userEmail) {
28-
throw new HttpError(
29-
403,
30-
'User needs an email to make a payment. If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.'
31-
);
28+
// If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.
29+
throw new HttpError(403, 'User needs an email to make a payment.');
3230
}
3331

3432
const paymentPlan = paymentPlans[paymentPlanId];

0 commit comments

Comments
 (0)