@@ -7,6 +7,11 @@ import * as log from 'log'
77import * as logger from 'log-aws-lambda'
88import * as jwt from 'jsonwebtoken'
99import Stripe from 'stripe'
10+ import {
11+ Environment ,
12+ Paddle ,
13+ EventName as PaddleEventName ,
14+ } from '@paddle/paddle-node-sdk'
1015
1116import { fetchProfile , isProd , proactivelyUndiscoverDevices } from './helper'
1217import AWS = require( 'aws-sdk' )
@@ -21,16 +26,24 @@ import {
2126} from './version'
2227import { Plan , PlanName } from './Plan'
2328import {
24- handleCheckoutSessionCompleted ,
25- handleCustomerSubscriptionDeleted ,
26- handleInvoicePaymentFailed ,
27- handleInvoicePaymentSucceeded ,
29+ handlePaddleSubscriptionActivated ,
30+ handlePaddleSubscriptionCanceled ,
31+ handlePaddleSubscriptionPastDue ,
32+ handlePaddleTransactionCompleted ,
33+ handleStripeCheckoutSessionCompleted ,
34+ handleStripeCustomerSubscriptionDeleted ,
35+ handleStripeInvoicePaymentFailed ,
36+ handleStripeInvoicePaymentSucceeded ,
2837} from './subscription'
2938
3039const stripe = new Stripe ( process . env . STRIPE_API_KEY , {
3140 apiVersion : '2023-08-16' ,
3241} )
3342
43+ const paddle = new Paddle ( process . env . PADDLE_API_KEY , {
44+ environment : isProd ( ) ? Environment . production : Environment . sandbox ,
45+ } )
46+
3447interface AuthenticatedRequest extends express . Request {
3548 userId : string
3649 jwt ?: { [ key : string ] : any }
@@ -176,24 +189,28 @@ app.post('/stripe_webhook', async function (req, res) {
176189
177190 switch ( event . type ) {
178191 case 'checkout.session.completed' : //https://stripe.com/docs/api/checkout/sessions/object
179- await handleCheckoutSessionCompleted (
192+ await handleStripeCheckoutSessionCompleted (
180193 event . data . object as Stripe . Checkout . Session
181194 )
182195 break
183196 case 'customer.subscription.deleted' : //https://stripe.com/docs/api/subscriptions/object
184- await handleCustomerSubscriptionDeleted (
197+ await handleStripeCustomerSubscriptionDeleted (
185198 event . data . object as Stripe . Subscription
186199 )
187200 break
188201 case 'invoice.payment_succeeded' : //https://stripe.com/docs/api/invoices/object
189- await handleInvoicePaymentSucceeded ( event . data . object as Stripe . Invoice )
202+ await handleStripeInvoicePaymentSucceeded (
203+ event . data . object as Stripe . Invoice
204+ )
190205 break
191206 case 'invoice.payment_failed' : //https://stripe.com/docs/api/invoices/object
192- await handleInvoicePaymentFailed ( event . data . object as Stripe . Invoice )
207+ await handleStripeInvoicePaymentFailed (
208+ event . data . object as Stripe . Invoice
209+ )
193210 break
194211 // ... handle other event types
195212 default :
196- log . warn ( `Unhandled event type ${ event . type } : %j` , event )
213+ log . warn ( `Unhandled Stripe event type ${ event . type } : %j` , event )
197214 }
198215
199216 // Return a 200 response to acknowledge receipt of the event
@@ -204,6 +221,52 @@ app.post('/stripe_webhook', async function (req, res) {
204221 }
205222} )
206223
224+ app . post ( '/paddle_webhook' , async function ( req , res ) {
225+ const signature = ( req . headers [ 'paddle-signature' ] as string ) || ''
226+ // req.body should be of type `buffer`, convert to string before passing it to `unmarshal`.
227+ // If express returned a JSON, remove any other middleware that might have processed raw request to object
228+ const rawRequestBody = ( req as any ) . rawBody
229+ // Replace `WEBHOOK_SECRET_KEY` with the secret key in notifications from vendor dashboard
230+ const secretKey = process . env . PADDLE_WEBHOOK_SECRET || ''
231+
232+ try {
233+ if ( signature && rawRequestBody ) {
234+ // The `unmarshal` function will validate the integrity of the webhook and return an entity
235+ const eventData = await paddle . webhooks . unmarshal (
236+ rawRequestBody ,
237+ secretKey ,
238+ signature
239+ )
240+ switch ( eventData . eventType ) {
241+ case PaddleEventName . SubscriptionActivated :
242+ await handlePaddleSubscriptionActivated ( eventData )
243+ break
244+ case PaddleEventName . SubscriptionCanceled :
245+ await handlePaddleSubscriptionCanceled ( eventData )
246+ break
247+ case PaddleEventName . SubscriptionPastDue :
248+ await handlePaddleSubscriptionPastDue ( eventData )
249+ break
250+ case PaddleEventName . TransactionCompleted :
251+ await handlePaddleTransactionCompleted ( eventData )
252+ break
253+ default :
254+ log . warn (
255+ `Unhandled Paddle event type ${ eventData . eventType } : %j` ,
256+ eventData
257+ )
258+ }
259+ } else {
260+ console . log ( 'Signature missing in header' )
261+ }
262+ } catch ( e ) {
263+ // Handle signature mismatch or other runtime errors
264+ console . log ( e )
265+ }
266+ // Return a response to acknowledge
267+ res . send ( 'Processed Paddle webhook event' )
268+ } )
269+
207270//applying middlewares for all endpoints below these lines!
208271app . use ( cors ( ) )
209272app . use ( express . urlencoded ( { extended : true } ) ) // for parsing application/x-www-form-urlencoded
@@ -404,29 +467,31 @@ app.get('/plan', needsAuth, async function (req: AuthenticatedRequest, res) {
404467 priceTags : [
405468 {
406469 name : 'vsh-pro-yearly' ,
407- tag : '12 EUR per year' ,
470+ tag : '14 EUR per year' ,
408471 checkoutToken : jwt . sign (
409472 {
410473 aud : 'checkout' ,
411474 sub : req . userId ,
475+ productName : 'VSH PRO - Yearly' ,
412476 priceId : isProd ( )
413- ? 'price_1Li1C4C3eSYquofeqstbOGi9 '
414- : 'price_1LgSpdC3eSYquofeNk3MClG1 ' ,
477+ ? 'pri_01jtxvka1st9m2m6bns9cha8hp '
478+ : 'pri_01jtwt97ekjxb7xn6q1a9p8g5g ' ,
415479 } ,
416480 process . env . HASH_SECRET ,
417481 { expiresIn : '30m' }
418482 ) ,
419483 } ,
420484 {
421485 name : 'vsh-pro-monthly' ,
422- tag : '1.49 EUR per month' ,
486+ tag : '1.79 EUR per month' ,
423487 checkoutToken : jwt . sign (
424488 {
425489 aud : 'checkout' ,
426490 sub : req . userId ,
491+ productName : 'VSH PRO - Monthly' ,
427492 priceId : isProd ( )
428- ? 'price_1Li1C4C3eSYquofekuqBmkqk '
429- : 'price_1LgSpdC3eSYquofegmyiZdQv ' ,
493+ ? 'pri_01jtxvmaxj28qyjwyfbz6qrby4 '
494+ : 'pri_01jtwt8adq0rh6t605dc4b4avn ' ,
430495 } ,
431496 process . env . HASH_SECRET ,
432497 { expiresIn : '30m' }
@@ -446,47 +511,80 @@ app.get(
446511 '/checkout' ,
447512 needsTokenForAudience ( 'checkout' ) ,
448513 async function ( req : AuthenticatedRequest , res ) {
449- const { stripeCustomerId, email } = await getUserRecord ( req . userId )
450-
451- //init Stripe checkout session and redirect to their checkout experience
452- const stripeSession = await stripe . checkout . sessions . create ( {
453- mode : 'subscription' ,
454- client_reference_id : req . userId ,
455- ...( stripeCustomerId && { customer : stripeCustomerId } ) , //include customer property if stripeCustomerId is truthy
456- ...( ! stripeCustomerId && { customer_email : email } ) , //include customer_email property if stripeCustomerId is falsy
457- line_items : [
458- {
459- price : req . jwt . priceId ,
460- quantity : 1 ,
461- } ,
462- ] ,
463- allow_promotion_codes : true ,
464- // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
465- // the actual Session ID is returned in the query parameter when your customer
466- // is redirected to the success page.
467- success_url : `https://${ req . hostname } /dev/stripe_redirect?session_id={CHECKOUT_SESSION_ID}` ,
468- cancel_url : `https://${ req . hostname } /dev/stripe_redirect?cancelled=true` ,
469- } )
514+ // redirect to the checkout page that uses Paddle Billing
515+ const { email } = await getUserRecord ( req . userId )
516+
517+ const checkoutIntend = {
518+ userId : req . userId ,
519+ email : email ,
520+ priceId : req . jwt . priceId ,
521+ jwt : req . query . token ,
522+ sandbox : ! isProd ( ) ,
523+ productName : req . jwt . productName ,
524+ }
470525
471- // Redirect to the URL returned on the Checkout Session.
472- res . redirect ( 303 , stripeSession . url )
526+ res . redirect (
527+ 303 ,
528+ 'https://vsh.csuermann.de/checkout/?checkoutIntend=' +
529+ encode ( JSON . stringify ( checkoutIntend ) )
530+ )
531+
532+ // before 2025-05-10 we used Stripe
533+ // const { stripeCustomerId, email } = await getUserRecord(req.userId)
534+ // //init Stripe checkout session and redirect to their checkout experience
535+ // const stripeSession = await stripe.checkout.sessions.create({
536+ // mode: 'subscription',
537+ // client_reference_id: req.userId,
538+ // ...(stripeCustomerId && { customer: stripeCustomerId }), //include customer property if stripeCustomerId is truthy
539+ // ...(!stripeCustomerId && { customer_email: email }), //include customer_email property if stripeCustomerId is falsy
540+ // line_items: [
541+ // {
542+ // price: req.jwt.priceId,
543+ // quantity: 1,
544+ // },
545+ // ],
546+ // allow_promotion_codes: true,
547+ // // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
548+ // // the actual Session ID is returned in the query parameter when your customer
549+ // // is redirected to the success page.
550+ // success_url: `https://${req.hostname}/dev/stripe_redirect?session_id={CHECKOUT_SESSION_ID}`,
551+ // cancel_url: `https://${req.hostname}/dev/stripe_redirect?cancelled=true`,
552+ // })
553+
554+ // // Redirect to the URL returned on the Checkout Session.
555+ // res.redirect(303, stripeSession.url)
473556 }
474557)
475558
476559app . get (
477560 '/subscription' ,
478561 needsTokenForAudience ( 'subscription' ) ,
479562 async function ( req : AuthenticatedRequest , res ) {
480- const { stripeCustomerId } = await getUserRecord ( req . userId )
563+ const { stripeCustomerId, paddleCustomerId } = await getUserRecord (
564+ req . userId
565+ )
481566
482- //init Stripe customer portal session and redirect to there
483- const portalSession = await stripe . billingPortal . sessions . create ( {
484- customer : stripeCustomerId ,
485- return_url : `https://${ req . hostname } /dev/stripe_redirect` ,
486- } )
567+ if ( paddleCustomerId ) {
568+ const paddlePortalSession = await paddle . customerPortalSessions . create (
569+ paddleCustomerId ,
570+ [ ]
571+ )
572+
573+ return res . redirect ( 303 , paddlePortalSession . urls . general . overview )
574+ }
575+
576+ if ( stripeCustomerId ) {
577+ //init Stripe customer portal session and redirect to there
578+ const portalSession = await stripe . billingPortal . sessions . create ( {
579+ customer : stripeCustomerId ,
580+ return_url : `https://${ req . hostname } /dev/stripe_redirect` ,
581+ } )
582+
583+ // Redirect to the URL returned on the portal session.
584+ return res . redirect ( 303 , portalSession . url )
585+ }
487586
488- // Redirect to the URL returned on the portal session.
489- res . redirect ( 303 , portalSession . url )
587+ res . status ( 400 ) . send ( { error : 'no payment provider customer found' } )
490588 }
491589)
492590
0 commit comments