@@ -33,6 +33,14 @@ class SubscriptionInfo {
33
33
return 'pi-hydration/subscription_cancelled'
34
34
}
35
35
36
+ static get HYDRATION_UNAUTHORIZED ( ) {
37
+ return 'pi-hydration/unauthorized'
38
+ }
39
+
40
+ static get HYDRATION_BAD_REQUEST ( ) {
41
+ return 'pi-hydration/bad_request'
42
+ }
43
+
36
44
/**
37
45
* @typedef SubscriptionInfos
38
46
* @property {SubscriptionInfo } [any] subscription info per subscription plan id
@@ -439,9 +447,90 @@ class SubscriptionInfo {
439
447
* @param {Object } subscription the current local subscription status instance
440
448
* @param {String } checkoutId checkout id of paddle.com
441
449
* @throws Error if hydration failed unexepectedly
450
+ * @throws SubscriptionInfo.HYDRATION_BAD_REQUEST if the subscription going to be hydrated contains no client information in custom_data
451
+ * @throws SubscriptionInfo.HYDRATION_UNAUTHORIZED if ids do not match the ids found in the subscription
442
452
* @returns
443
453
*/
444
454
async hydrateSubscriptionCreated ( ids , { subscription_id } , checkoutId ) {
455
+ {
456
+ const { subscription } = await this . _storage . get ( ids )
457
+ if ( subscription ?. status ?. length > 1 ) {
458
+ return
459
+ }
460
+ }
461
+
462
+ const subscriptions = await this . _api . getSubscription ( { subscription_id } )
463
+ if ( ! Array . isArray ( subscriptions ) || subscriptions . length < 1 ) {
464
+ return
465
+ }
466
+
467
+ // more sanity checks here
468
+ const subscription = subscriptions . at ( 0 )
469
+ if ( ! subscription . custom_data ?. _pi ?. ids ) {
470
+ throw new Error ( SubscriptionInfo . HYDRATION_BAD_REQUEST )
471
+ }
472
+
473
+ const idsFromCustomData = subscription . custom_data . _pi . ids
474
+ if ( ! Array . isArray ( idsFromCustomData ) ) {
475
+ throw new Error ( SubscriptionInfo . HYDRATION_UNAUTHORIZED )
476
+ }
477
+
478
+ const isForTargetId = ids . length === idsFromCustomData . length && idsFromCustomData . every ( ( id , index ) => id === ids [ index ] )
479
+ if ( ! isForTargetId ) {
480
+ throw new Error ( SubscriptionInfo . HYDRATION_UNAUTHORIZED )
481
+ }
482
+
483
+ const hookPayload = {
484
+ alert_id : SubscriptionInfo . HYDRATION_SUBSCRIPTION_CREATED ,
485
+ alert_name : SubscriptionInfo . HYDRATION_SUBSCRIPTION_CREATED ,
486
+ currency : subscription . last_payment . currency ,
487
+ status : subscription . state ,
488
+ next_bill_date : subscription . next_payment ?. date || '' ,
489
+ quantity : subscription . quantity ,
490
+ event_time : subscription . signup_date ,
491
+ source : 'pi-hydration' ,
492
+ update_url : subscription . update_url ,
493
+ subscription_id : subscription . subscription_id ,
494
+ subscription_plan_id : subscription . plan_id ,
495
+ cancel_url : subscription . cancel_url ,
496
+ checkout_id : checkoutId ,
497
+ user_id : subscription . user_id ,
498
+ // use the ids from paddle api here
499
+ // these were initially passed during checkout and cannot be updated
500
+ // thus we ensure that user do not hydrate other subscriptions too
501
+ passthrough : JSON . stringify ( { ids : idsFromCustomData } )
502
+ }
503
+
504
+ if ( subscription . state === 'active' ) {
505
+ await this . _hookStorage . addSubscriptionCreatedStatus ( hookPayload )
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Fetches the latest subscription status from paddle API and updates the local status accordingly.
511
+ *
512
+ * This implementation is cautious in that it only updates the status if
513
+ * - current status returned by API is active
514
+ * - local status' contain only the pre-checkout placeholder
515
+ *
516
+ * If update of the local subscription status is not necessary - e.g. because
517
+ * the webhook was also already received - the method will just silently return.
518
+ *
519
+ * This method allows us to decouple ourselves from the timely arrival of paddle webhooks. Because
520
+ * webhooks are necessary to store a subscription created event in our database. If the webhook
521
+ * does not arrive in time, our users need to wait for a finite amount of time which
522
+ * is not a convincing user experience.
523
+ *
524
+ * This method can be called after the first checkout and after the order was processsed
525
+ * to already store subscription-related data and let the user already enjoy some goodies.
526
+ *
527
+ * @param {Array } ids ids pointing to the target subscription object
528
+ * @param {Object } subscription the current local subscription status instance
529
+ * @param {String } checkoutId checkout id of paddle.com
530
+ * @throws Error if hydration failed unexepectedly
531
+ * @returns
532
+ */
533
+ async hydrateSubscriptionCreated2 ( ids , { orderId, email } ) {
445
534
{
446
535
const { subscription } = await this . _storage . get ( ids )
447
536
0 commit comments