@@ -7,6 +7,7 @@ import * as log from 'log'
77import * as logger from 'log-aws-lambda'
88import * as jwt from 'jsonwebtoken'
99import Stripe from 'stripe'
10+ import { Paddle , EventName as PaddleEventName } from '@paddle/paddle-node-sdk'
1011
1112import { fetchProfile , isProd , proactivelyUndiscoverDevices } from './helper'
1213import AWS = require( 'aws-sdk' )
@@ -31,6 +32,8 @@ const stripe = new Stripe(process.env.STRIPE_API_KEY, {
3132 apiVersion : '2023-08-16' ,
3233} )
3334
35+ const paddle = new Paddle ( process . env . PADDLE_API_KEY )
36+
3437interface AuthenticatedRequest extends express . Request {
3538 userId : string
3639 jwt ?: { [ key : string ] : any }
@@ -193,7 +196,7 @@ app.post('/stripe_webhook', async function (req, res) {
193196 break
194197 // ... handle other event types
195198 default :
196- log . warn ( `Unhandled event type ${ event . type } : %j` , event )
199+ log . warn ( `Unhandled Stripe event type ${ event . type } : %j` , event )
197200 }
198201
199202 // Return a 200 response to acknowledge receipt of the event
@@ -204,6 +207,46 @@ app.post('/stripe_webhook', async function (req, res) {
204207 }
205208} )
206209
210+ app . post ( '/paddle_webhook' , async function ( req , res ) {
211+ const signature = ( req . headers [ 'paddle-signature' ] as string ) || ''
212+ // req.body should be of type `buffer`, convert to string before passing it to `unmarshal`.
213+ // If express returned a JSON, remove any other middleware that might have processed raw request to object
214+ const rawRequestBody = ( req as any ) . rawBody
215+ // Replace `WEBHOOK_SECRET_KEY` with the secret key in notifications from vendor dashboard
216+ const secretKey = process . env . PADDLE_WEBHOOK_SECRET || ''
217+
218+ try {
219+ if ( signature && rawRequestBody ) {
220+ // The `unmarshal` function will validate the integrity of the webhook and return an entity
221+ const eventData = await paddle . webhooks . unmarshal (
222+ rawRequestBody ,
223+ secretKey ,
224+ signature
225+ )
226+ switch ( eventData . eventType ) {
227+ // case PaddleEventName.ProductUpdated:
228+ // console.log(`Product ${eventData.data.id} was updated`)
229+ // break
230+ // case PaddleEventName.SubscriptionUpdated:
231+ // console.log(`Subscription ${eventData.data.id} was updated`)
232+ // break
233+ default :
234+ log . warn (
235+ `Unhandled Paddle event type ${ eventData . eventType } : %j` ,
236+ eventData
237+ )
238+ }
239+ } else {
240+ console . log ( 'Signature missing in header' )
241+ }
242+ } catch ( e ) {
243+ // Handle signature mismatch or other runtime errors
244+ console . log ( e )
245+ }
246+ // Return a response to acknowledge
247+ res . send ( 'Processed Paddle webhook event' )
248+ } )
249+
207250//applying middlewares for all endpoints below these lines!
208251app . use ( cors ( ) )
209252app . use ( express . urlencoded ( { extended : true } ) ) // for parsing application/x-www-form-urlencoded
@@ -409,9 +452,10 @@ app.get('/plan', needsAuth, async function (req: AuthenticatedRequest, res) {
409452 {
410453 aud : 'checkout' ,
411454 sub : req . userId ,
455+ productName : 'VSH PRO - Yearly' ,
412456 priceId : isProd ( )
413- ? 'price_1Li1C4C3eSYquofeqstbOGi9 '
414- : 'price_1LgSpdC3eSYquofeNk3MClG1 ' ,
457+ ? 'pri_01jtxvka1st9m2m6bns9cha8hp '
458+ : 'pri_01jtwt97ekjxb7xn6q1a9p8g5g ' ,
415459 } ,
416460 process . env . HASH_SECRET ,
417461 { expiresIn : '30m' }
@@ -424,9 +468,10 @@ app.get('/plan', needsAuth, async function (req: AuthenticatedRequest, res) {
424468 {
425469 aud : 'checkout' ,
426470 sub : req . userId ,
471+ productName : 'VSH PRO - Monthly' ,
427472 priceId : isProd ( )
428- ? 'price_1Li1C4C3eSYquofekuqBmkqk '
429- : 'price_1LgSpdC3eSYquofegmyiZdQv ' ,
473+ ? 'pri_01jtxvmaxj28qyjwyfbz6qrby4 '
474+ : 'pri_01jtwt8adq0rh6t605dc4b4avn ' ,
430475 } ,
431476 process . env . HASH_SECRET ,
432477 { expiresIn : '30m' }
@@ -446,47 +491,80 @@ app.get(
446491 '/checkout' ,
447492 needsTokenForAudience ( 'checkout' ) ,
448493 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- } )
494+ // redirect to the checkout page that uses Paddle Billing
495+ const { email } = await getUserRecord ( req . userId )
496+
497+ const checkoutIntend = {
498+ userId : req . userId ,
499+ email : email ,
500+ priceId : req . jwt . priceId ,
501+ jwt : req . jwt ,
502+ sandbox : ! isProd ( ) ,
503+ productName : req . jwt . productName ,
504+ }
505+
506+ res . redirect (
507+ 303 ,
508+ 'https://vsh.csuermann.de/checkout/?checkoutIntend=' +
509+ encode ( JSON . stringify ( checkoutIntend ) )
510+ )
470511
471- // Redirect to the URL returned on the Checkout Session.
472- res . redirect ( 303 , stripeSession . url )
512+ // before 2025-05-10 we used Stripe
513+ // const { stripeCustomerId, email } = await getUserRecord(req.userId)
514+ // //init Stripe checkout session and redirect to their checkout experience
515+ // const stripeSession = await stripe.checkout.sessions.create({
516+ // mode: 'subscription',
517+ // client_reference_id: req.userId,
518+ // ...(stripeCustomerId && { customer: stripeCustomerId }), //include customer property if stripeCustomerId is truthy
519+ // ...(!stripeCustomerId && { customer_email: email }), //include customer_email property if stripeCustomerId is falsy
520+ // line_items: [
521+ // {
522+ // price: req.jwt.priceId,
523+ // quantity: 1,
524+ // },
525+ // ],
526+ // allow_promotion_codes: true,
527+ // // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
528+ // // the actual Session ID is returned in the query parameter when your customer
529+ // // is redirected to the success page.
530+ // success_url: `https://${req.hostname}/dev/stripe_redirect?session_id={CHECKOUT_SESSION_ID}`,
531+ // cancel_url: `https://${req.hostname}/dev/stripe_redirect?cancelled=true`,
532+ // })
533+
534+ // // Redirect to the URL returned on the Checkout Session.
535+ // res.redirect(303, stripeSession.url)
473536 }
474537)
475538
476539app . get (
477540 '/subscription' ,
478541 needsTokenForAudience ( 'subscription' ) ,
479542 async function ( req : AuthenticatedRequest , res ) {
480- const { stripeCustomerId } = await getUserRecord ( req . userId )
543+ const { stripeCustomerId, paddleCustomerId } = await getUserRecord (
544+ req . userId
545+ )
481546
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- } )
547+ if ( paddleCustomerId ) {
548+ const paddlePortalSession = await paddle . customerPortalSessions . create (
549+ paddleCustomerId ,
550+ [ ]
551+ )
552+
553+ return res . redirect ( 303 , paddlePortalSession . urls . general . overview )
554+ }
555+
556+ if ( stripeCustomerId ) {
557+ //init Stripe customer portal session and redirect to there
558+ const portalSession = await stripe . billingPortal . sessions . create ( {
559+ customer : stripeCustomerId ,
560+ return_url : `https://${ req . hostname } /dev/stripe_redirect` ,
561+ } )
562+
563+ // Redirect to the URL returned on the portal session.
564+ return res . redirect ( 303 , portalSession . url )
565+ }
487566
488- // Redirect to the URL returned on the portal session.
489- res . redirect ( 303 , portalSession . url )
567+ res . status ( 400 ) . send ( { error : 'no payment provider customer found' } )
490568 }
491569)
492570
0 commit comments