@@ -8,180 +8,180 @@ import { Webhook } from 'svix';
8
8
const stripe = new Stripe ( process . env . STRIPE_SECRET_KEY ?? "" ) ;
9
9
const resend = new Resend ( process . env . RESEND_API_KEY ?? "" )
10
10
export type OnboardingPayload = {
11
- event : string ;
12
- clerkUserId : string ;
13
- email : string ;
14
- firstName : string ;
15
- lastName : string ;
11
+ event : string ;
12
+ clerkUserId : string ;
13
+ email : string ;
14
+ firstName : string ;
15
+ lastName : string ;
16
16
}
17
17
18
18
async function validateRequest ( payloadString : string , headerPayload : Headers ) {
19
19
20
- const svixHeaders = {
21
- "svix-id" : headerPayload . get ( "svix-id" ) as string ,
22
- "svix-timestamp" : headerPayload . get ( "svix-timestamp" ) as string ,
23
- "svix-signature" : headerPayload . get ( "svix-signature" ) as string ,
24
- }
25
- const wh = new Webhook ( process . env . CLERK_WEBHOOK_SECRET ?? "" ) ;
26
- return wh . verify ( payloadString , svixHeaders ) as WebhookEvent ;
20
+ const svixHeaders = {
21
+ "svix-id" : headerPayload . get ( "svix-id" ) as string ,
22
+ "svix-timestamp" : headerPayload . get ( "svix-timestamp" ) as string ,
23
+ "svix-signature" : headerPayload . get ( "svix-signature" ) as string ,
24
+ }
25
+ const wh = new Webhook ( process . env . CLERK_WEBHOOK_SECRET ?? "" ) ;
26
+ return wh . verify ( payloadString , svixHeaders ) as WebhookEvent ;
27
27
}
28
28
29
29
export const { POST } = serve < string > ( async ( context ) => {
30
- const payloadString = context . requestPayload ;
31
- const headerPayload = context . headers ;
32
-
33
- let event : WebhookEvent ;
34
- try {
35
- event = await validateRequest ( payloadString , headerPayload ) ;
36
- } catch {
37
- return
38
- }
39
-
40
- const user = await context . run < false | OnboardingPayload > ( "handle-clerk-webhook-event" , async ( ) => {
41
- if ( event . type === "user.created" ) {
42
- const { id : clerkUserId , email_addresses, first_name, last_name } = event . data ;
43
- const primaryEmail = email_addresses . find ( email => email . id === event . data . primary_email_address_id )
44
-
45
- if ( ! primaryEmail ) {
46
- return false
47
- }
48
-
49
- return {
50
- event : event . type ,
51
- clerkUserId : clerkUserId ,
52
- email : primaryEmail . email_address ,
53
- firstName : first_name ,
54
- lastName : last_name ,
55
- } as OnboardingPayload
56
- }
57
- return false
58
- } )
59
-
60
- if ( ! user ) {
61
- return
62
- }
63
-
64
- const customer = await context . run ( "create-stripe-customer" , async ( ) => {
65
- return await stripe . customers . create ( {
66
- email : user . email ,
67
- name : `${ user . firstName } ${ user . lastName } ` ,
68
- metadata : {
69
- clerkUserId : user . clerkUserId
70
- }
71
- } )
72
- } )
73
-
74
- await context . run ( "send-welcome-email" , async ( ) => {
75
- console . log ( "Sending welcome email to:" , user . email )
76
-
77
- await resend . emails . send ( {
78
-
79
- to : user . email ,
80
- subject : 'Welcome to Your Trial!' ,
81
- html : `
82
- <h1>Welcome ${ user . firstName || 'there' } !</h1>
83
- <p>Thanks for signing up! Your trial starts now.</p>
84
- <p>You have 7 days to explore all our premium features.</p>
85
- <p>What you get with your trial:</p>
86
- <ul>
87
- <li>Feature 1</li>
88
- <li>Feature 2</li>
89
- <li>Feature 3</li>
90
- </ul>
91
- <p>Get started now: <a href="${ process . env . NEXT_PUBLIC_URL } /dashboard">Visit Dashboard</a></p>
92
- `
93
- } ) ;
94
-
95
- } )
96
-
97
- const subscription = await context . run ( "create-trial" , async ( ) => {
98
- return await stripe . subscriptions . create ( {
99
- customer : customer . id ,
100
- items : [ { price : "price_1QQQWaCKnqweyLP9MPbARyG" } ] ,
101
- trial_period_days : 7 ,
102
- metadata : {
103
- clerkUserId : user . clerkUserId ,
104
- workflowRunId : context . workflowRunId
105
- }
106
- } )
107
- } )
108
-
109
- await context . run ( "store-subscription" , async ( ) => {
110
- console . log ( subscription )
111
- } )
112
-
113
-
114
- /**
115
- * This is where we start waiting for the payment method to be added to the subscription.
116
- * If the payment method is added within 7 days, workflow on the `api/stripe/route` will notify this workflow with `payment_method_<SUBSCRIPTION_ID>`.
117
- * If the payment method is not added within 7 days, we will handle the trial end.
118
- */
119
- const { timeout } = await context . waitForEvent ( "await-payment-method" , `payment_method_${ subscription . id } ` , {
120
- timeout : "7d"
121
- } )
122
-
123
-
124
- if ( ! timeout ) {
125
- await context . run ( "send-subscription-start-welcome-mail" , async ( ) => {
126
- console . log ( "Sending subscription started email to:" , user . email )
127
-
128
- await resend . emails . send ( {
129
-
130
- to : user . email ,
131
- subject : 'Payment Method Added Successfully!' ,
132
- html : `
133
- <h1>Thank you for adding your payment method!</h1>
134
- <p>Your subscription will continue automatically after the trial period.</p>
135
- <p>Your trial benefits:</p>
136
- <ul>
137
- <li>Unlimited access to all features</li>
138
- <li>Priority support</li>
139
- <li>No interruption in service</li>
140
- </ul>
141
- <p>Need help? Reply to this email or visit our support center.</p>
142
- `
143
- } ) ;
144
- } )
145
-
146
- } else {
147
- await context . run ( "handle-trial-end" , async ( ) => {
148
- await stripe . subscriptions . update ( subscription . id , {
149
- cancel_at_period_end : true
150
- } )
151
-
152
- return { status : 'trial_ended' }
153
- } )
154
-
155
-
156
- await context . run ( "send-trial-ending-mail" , async ( ) => {
157
- console . log ( "Sending trial ending email to:" , user . email )
158
-
159
- await resend . emails . send ( {
160
-
161
- to : user . email ,
162
- subject : 'Your Trial is Ending Soon' ,
163
- html : `
164
- <h1>Don't Lose Access!</h1>
165
- <p>Your trial is coming to an end. Add a payment method to keep your access:</p>
166
- <ul>
167
- <li>Keep all your data and settings</li>
168
- <li>Continue using premium features</li>
169
- <li>No interruption in service</li>
170
- </ul>
171
- <a href="${ process . env . NEXT_PUBLIC_URL } /billing" style="
172
- display: inline-block;
173
- padding: 12px 24px;
174
- background-color: #0070f3;
175
- color: white;
176
- text-decoration: none;
177
- border-radius: 5px;
178
- margin: 20px 0;
179
- ">Add Payment Method</a>
180
- <p>Questions? Contact our support team!</p>
181
- `
182
- } ) ;
183
-
184
- } )
185
- }
30
+ const payloadString = context . requestPayload ;
31
+ const headerPayload = context . headers ;
32
+
33
+ let event : WebhookEvent ;
34
+ try {
35
+ event = await validateRequest ( payloadString , headerPayload ) ;
36
+ } catch {
37
+ return
38
+ }
39
+
40
+ const user = await context . run < false | OnboardingPayload > ( "handle-clerk-webhook-event" , async ( ) => {
41
+ if ( event . type === "user.created" ) {
42
+ const { id : clerkUserId , email_addresses, first_name, last_name } = event . data ;
43
+ const primaryEmail = email_addresses . find ( email => email . id === event . data . primary_email_address_id )
44
+
45
+ if ( ! primaryEmail ) {
46
+ return false
47
+ }
48
+
49
+ return {
50
+ event : event . type ,
51
+ clerkUserId : clerkUserId ,
52
+ email : primaryEmail . email_address ,
53
+ firstName : first_name ,
54
+ lastName : last_name ,
55
+ } as OnboardingPayload
56
+ }
57
+ return false
58
+ } )
59
+
60
+ if ( ! user ) {
61
+ return
62
+ }
63
+
64
+ const customer = await context . run ( "create-stripe-customer" , async ( ) => {
65
+ return await stripe . customers . create ( {
66
+ email : user . email ,
67
+ name : `${ user . firstName } ${ user . lastName } ` ,
68
+ metadata : {
69
+ clerkUserId : user . clerkUserId
70
+ }
71
+ } )
72
+ } )
73
+
74
+ await context . run ( "send-welcome-email" , async ( ) => {
75
+ console . log ( "Sending welcome email to:" , user . email )
76
+
77
+ await resend . emails . send ( {
78
+
79
+ to : user . email ,
80
+ subject : 'Welcome to Your Trial!' ,
81
+ html : `
82
+ <h1>Welcome ${ user . firstName || 'there' } !</h1>
83
+ <p>Thanks for signing up! Your trial starts now.</p>
84
+ <p>You have 7 days to explore all our premium features.</p>
85
+ <p>What you get with your trial:</p>
86
+ <ul>
87
+ <li>Feature 1</li>
88
+ <li>Feature 2</li>
89
+ <li>Feature 3</li>
90
+ </ul>
91
+ <p>Get started now: <a href="${ process . env . NEXT_PUBLIC_URL } /dashboard">Visit Dashboard</a></p>
92
+ `
93
+ } ) ;
94
+
95
+ } )
96
+
97
+ const subscription = await context . run ( "create-trial" , async ( ) => {
98
+ return await stripe . subscriptions . create ( {
99
+ customer : customer . id ,
100
+ items : [ { price : "price_1QQQWaCKnqweyLP9MPbARyG" } ] ,
101
+ trial_period_days : 7 ,
102
+ metadata : {
103
+ clerkUserId : user . clerkUserId ,
104
+ workflowRunId : context . workflowRunId
105
+ }
106
+ } )
107
+ } )
108
+
109
+ await context . run ( "store-subscription" , async ( ) => {
110
+ console . log ( subscription )
111
+ } )
112
+
113
+
114
+ /**
115
+ * This is where we start waiting for the payment method to be added to the subscription.
116
+ * If the payment method is added within 7 days, workflow on the `api/stripe/route` will notify this workflow with `payment_method_<SUBSCRIPTION_ID>`.
117
+ * If the payment method is not added within 7 days, we will handle the trial end.
118
+ */
119
+ const { timeout } = await context . waitForEvent ( "await-payment-method" , `payment_method_${ subscription . id } ` , {
120
+ timeout : "7d"
121
+ } )
122
+
123
+
124
+ if ( ! timeout ) {
125
+ await context . run ( "send-subscription-start-welcome-mail" , async ( ) => {
126
+ console . log ( "Sending subscription started email to:" , user . email )
127
+
128
+ await resend . emails . send ( {
129
+
130
+ to : user . email ,
131
+ subject : 'Payment Method Added Successfully!' ,
132
+ html : `
133
+ <h1>Thank you for adding your payment method!</h1>
134
+ <p>Your subscription will continue automatically after the trial period.</p>
135
+ <p>Your trial benefits:</p>
136
+ <ul>
137
+ <li>Unlimited access to all features</li>
138
+ <li>Priority support</li>
139
+ <li>No interruption in service</li>
140
+ </ul>
141
+ <p>Need help? Reply to this email or visit our support center.</p>
142
+ `
143
+ } ) ;
144
+ } )
145
+
146
+ } else {
147
+ await context . run ( "handle-trial-end" , async ( ) => {
148
+ await stripe . subscriptions . update ( subscription . id , {
149
+ cancel_at_period_end : true
150
+ } )
151
+
152
+ return { status : 'trial_ended' }
153
+ } )
154
+
155
+
156
+ await context . run ( "send-trial-ending-mail" , async ( ) => {
157
+ console . log ( "Sending trial ending email to:" , user . email )
158
+
159
+ await resend . emails . send ( {
160
+
161
+ to : user . email ,
162
+ subject : 'Your Trial is Ending Soon' ,
163
+ html : `
164
+ <h1>Don't Lose Access!</h1>
165
+ <p>Your trial is coming to an end. Add a payment method to keep your access:</p>
166
+ <ul>
167
+ <li>Keep all your data and settings</li>
168
+ <li>Continue using premium features</li>
169
+ <li>No interruption in service</li>
170
+ </ul>
171
+ <a href="${ process . env . NEXT_PUBLIC_URL } /billing" style="
172
+ display: inline-block;
173
+ padding: 12px 24px;
174
+ background-color: #0070f3;
175
+ color: white;
176
+ text-decoration: none;
177
+ border-radius: 5px;
178
+ margin: 20px 0;
179
+ ">Add Payment Method</a>
180
+ <p>Questions? Contact our support team!</p>
181
+ `
182
+ } ) ;
183
+
184
+ } )
185
+ }
186
186
187
187
} , { baseUrl : "<BASE_URL>" , initialPayloadParser : ( payload ) => { return payload } } )
0 commit comments