@shware/purchase 7.3.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/app-store/index.cjs +1 -0
  2. package/dist/app-store/index.d.cts +2 -2
  3. package/dist/app-store/index.d.mts +2 -2
  4. package/dist/app-store/index.mjs +2 -2
  5. package/dist/app-store/mapper.cjs +22 -0
  6. package/dist/app-store/mapper.cjs.map +1 -1
  7. package/dist/app-store/mapper.d.cts +17 -1
  8. package/dist/app-store/mapper.d.cts.map +1 -1
  9. package/dist/app-store/mapper.d.mts +17 -1
  10. package/dist/app-store/mapper.d.mts.map +1 -1
  11. package/dist/app-store/mapper.mjs +22 -1
  12. package/dist/app-store/mapper.mjs.map +1 -1
  13. package/dist/google-play/index.cjs +1 -0
  14. package/dist/google-play/index.d.cts +2 -2
  15. package/dist/google-play/index.d.mts +2 -2
  16. package/dist/google-play/index.mjs +2 -2
  17. package/dist/google-play/mapper.cjs +12 -0
  18. package/dist/google-play/mapper.cjs.map +1 -1
  19. package/dist/google-play/mapper.d.cts +10 -1
  20. package/dist/google-play/mapper.d.cts.map +1 -1
  21. package/dist/google-play/mapper.d.mts +10 -1
  22. package/dist/google-play/mapper.d.mts.map +1 -1
  23. package/dist/google-play/mapper.mjs +12 -1
  24. package/dist/google-play/mapper.mjs.map +1 -1
  25. package/dist/stripe/config.cjs +9 -0
  26. package/dist/stripe/config.cjs.map +1 -1
  27. package/dist/stripe/config.d.cts +6 -0
  28. package/dist/stripe/config.d.cts.map +1 -1
  29. package/dist/stripe/config.d.mts +6 -0
  30. package/dist/stripe/config.d.mts.map +1 -1
  31. package/dist/stripe/config.mjs +9 -0
  32. package/dist/stripe/config.mjs.map +1 -1
  33. package/dist/stripe/index.cjs +7 -0
  34. package/dist/stripe/index.d.cts +4 -2
  35. package/dist/stripe/index.d.mts +4 -2
  36. package/dist/stripe/index.mjs +4 -2
  37. package/dist/stripe/mapper.cjs +21 -2
  38. package/dist/stripe/mapper.cjs.map +1 -1
  39. package/dist/stripe/mapper.d.cts +9 -1
  40. package/dist/stripe/mapper.d.cts.map +1 -1
  41. package/dist/stripe/mapper.d.mts +9 -1
  42. package/dist/stripe/mapper.d.mts.map +1 -1
  43. package/dist/stripe/mapper.mjs +22 -4
  44. package/dist/stripe/mapper.mjs.map +1 -1
  45. package/dist/stripe/payment-method.cjs +34 -0
  46. package/dist/stripe/payment-method.cjs.map +1 -0
  47. package/dist/stripe/payment-method.d.cts +22 -0
  48. package/dist/stripe/payment-method.d.cts.map +1 -0
  49. package/dist/stripe/payment-method.d.mts +22 -0
  50. package/dist/stripe/payment-method.d.mts.map +1 -0
  51. package/dist/stripe/payment-method.mjs +32 -0
  52. package/dist/stripe/payment-method.mjs.map +1 -0
  53. package/dist/stripe/subscription.cjs +35 -0
  54. package/dist/stripe/subscription.cjs.map +1 -0
  55. package/dist/stripe/subscription.d.cts +33 -0
  56. package/dist/stripe/subscription.d.cts.map +1 -0
  57. package/dist/stripe/subscription.d.mts +33 -0
  58. package/dist/stripe/subscription.d.mts.map +1 -0
  59. package/dist/stripe/subscription.mjs +33 -0
  60. package/dist/stripe/subscription.mjs.map +1 -0
  61. package/package.json +1 -1
@@ -5,3 +5,4 @@ const require_app_store_mapper = require("./mapper.cjs");
5
5
  exports.AppStoreConfig = require_app_store_config.AppStoreConfig;
6
6
  exports.AppStoreNotificationHandler = require_app_store_handler.AppStoreNotificationHandler;
7
7
  exports.mapCancellationDetails = require_app_store_mapper.mapCancellationDetails;
8
+ exports.mapPaymentType = require_app_store_mapper.mapPaymentType;
@@ -1,4 +1,4 @@
1
1
  import { AppStoreConfig } from "./config.cjs";
2
2
  import { AppStoreNotificationEvent, AppStoreNotificationHandler } from "./handler.cjs";
3
- import { AppStoreCancellationSource, mapCancellationDetails } from "./mapper.cjs";
4
- export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, mapCancellationDetails };
3
+ import { AppStoreCancellationSource, AppStorePaymentSource, mapCancellationDetails, mapPaymentType } from "./mapper.cjs";
4
+ export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, type AppStorePaymentSource, mapCancellationDetails, mapPaymentType };
@@ -1,4 +1,4 @@
1
1
  import { AppStoreConfig } from "./config.mjs";
2
2
  import { AppStoreNotificationEvent, AppStoreNotificationHandler } from "./handler.mjs";
3
- import { AppStoreCancellationSource, mapCancellationDetails } from "./mapper.mjs";
4
- export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, mapCancellationDetails };
3
+ import { AppStoreCancellationSource, AppStorePaymentSource, mapCancellationDetails, mapPaymentType } from "./mapper.mjs";
4
+ export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, type AppStorePaymentSource, mapCancellationDetails, mapPaymentType };
@@ -1,4 +1,4 @@
1
1
  import { AppStoreConfig } from "./config.mjs";
2
2
  import { AppStoreNotificationHandler } from "./handler.mjs";
3
- import { mapCancellationDetails } from "./mapper.mjs";
4
- export { AppStoreConfig, AppStoreNotificationHandler, mapCancellationDetails };
3
+ import { mapCancellationDetails, mapPaymentType } from "./mapper.mjs";
4
+ export { AppStoreConfig, AppStoreNotificationHandler, mapCancellationDetails, mapPaymentType };
@@ -31,6 +31,27 @@ function mapCancellationDetails(source) {
31
31
  default: return null;
32
32
  }
33
33
  }
34
+ /**
35
+ * The payment a subscription notification's transaction is, if it is one: the first purchase
36
+ * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
37
+ * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
38
+ * full and starts a new period at once, refunding what is left of the old one separately, so it is
39
+ * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
40
+ * and the like carry the current transaction, already paid, and are null.
41
+ *
42
+ * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
43
+ */
44
+ function mapPaymentType({ notificationType, subtype }) {
45
+ switch (notificationType) {
46
+ case "SUBSCRIBED": return "SUBSCRIPTION_START";
47
+ case "DID_RENEW": return "SUBSCRIPTION_RENEWAL";
48
+ case "DID_CHANGE_RENEWAL_PREF": return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
49
+ case "OFFER_REDEEMED":
50
+ if (subtype === "INITIAL_BUY" || subtype === "RESUBSCRIBE") return "SUBSCRIPTION_START";
51
+ return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
52
+ default: return null;
53
+ }
54
+ }
34
55
  /** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */
35
56
  function expirationIntentSubtype(intent) {
36
57
  switch (intent) {
@@ -43,5 +64,6 @@ function expirationIntentSubtype(intent) {
43
64
  }
44
65
  //#endregion
45
66
  exports.mapCancellationDetails = mapCancellationDetails;
67
+ exports.mapPaymentType = mapPaymentType;
46
68
 
47
69
  //# sourceMappingURL=mapper.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
1
+ {"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails, PaymentType } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The App Store Server Notification fields that say whether a notification is a payment. */\nexport interface AppStorePaymentSource {\n notificationType?: string | null;\n subtype?: string | null;\n}\n\n/**\n * The payment a subscription notification's transaction is, if it is one: the first purchase\n * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal\n * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in\n * full and starts a new period at once, refunding what is left of the old one separately, so it is\n * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change\n * and the like carry the current transaction, already paid, and are null.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n */\nexport function mapPaymentType({\n notificationType,\n subtype,\n}: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null {\n switch (notificationType) {\n case 'SUBSCRIBED':\n return 'SUBSCRIPTION_START';\n case 'DID_RENEW':\n return 'SUBSCRIPTION_RENEWAL';\n case 'DID_CHANGE_RENEWAL_PREF':\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n case 'OFFER_REDEEMED':\n if (subtype === 'INITIAL_BUY' || subtype === 'RESUBSCRIBE') return 'SUBSCRIPTION_START';\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;;;;;;;;;;AAkBA,SAAgB,eAAe,EAC7B,kBACA,WACiE;CACjE,QAAQ,kBAAR;EACE,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,2BACH,OAAO,YAAY,YAAY,yBAAyB;EAC1D,KAAK;GACH,IAAI,YAAY,iBAAiB,YAAY,eAAe,OAAO;GACnE,OAAO,YAAY,YAAY,yBAAyB;EAC1D,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
@@ -1,4 +1,4 @@
1
- import { CancellationDetails } from "../subscription/index.cjs";
1
+ import { CancellationDetails, PaymentType } from "../subscription/index.cjs";
2
2
  //#region src/app-store/mapper.d.ts
3
3
  /**
4
4
  * The App Store Server Notification fields that say why a subscription stops renewing: the
@@ -20,5 +20,21 @@ export interface AppStoreCancellationSource {
20
20
  * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)
21
21
  */
22
22
  export declare function mapCancellationDetails(source: AppStoreCancellationSource): CancellationDetails | null;
23
+ /** The App Store Server Notification fields that say whether a notification is a payment. */
24
+ export interface AppStorePaymentSource {
25
+ notificationType?: string | null;
26
+ subtype?: string | null;
27
+ }
28
+ /**
29
+ * The payment a subscription notification's transaction is, if it is one: the first purchase
30
+ * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
31
+ * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
32
+ * full and starts a new period at once, refunding what is left of the old one separately, so it is
33
+ * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
34
+ * and the like carry the current transaction, already paid, and are null.
35
+ *
36
+ * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
37
+ */
38
+ export declare function mapPaymentType({ notificationType, subtype }: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null;
23
39
  //#endregion
24
40
  //# sourceMappingURL=mapper.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP"}
1
+ {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP;;iBAoCc;EACf;EACA;;;;;;;;;;;;wBAac,iBACd,kBACA,WACC,wBAAwB,QAAQ"}
@@ -1,4 +1,4 @@
1
- import { CancellationDetails } from "../subscription/index.mjs";
1
+ import { CancellationDetails, PaymentType } from "../subscription/index.mjs";
2
2
  //#region src/app-store/mapper.d.ts
3
3
  /**
4
4
  * The App Store Server Notification fields that say why a subscription stops renewing: the
@@ -20,5 +20,21 @@ export interface AppStoreCancellationSource {
20
20
  * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)
21
21
  */
22
22
  export declare function mapCancellationDetails(source: AppStoreCancellationSource): CancellationDetails | null;
23
+ /** The App Store Server Notification fields that say whether a notification is a payment. */
24
+ export interface AppStorePaymentSource {
25
+ notificationType?: string | null;
26
+ subtype?: string | null;
27
+ }
28
+ /**
29
+ * The payment a subscription notification's transaction is, if it is one: the first purchase
30
+ * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
31
+ * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
32
+ * full and starts a new period at once, refunding what is left of the old one separately, so it is
33
+ * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
34
+ * and the like carry the current transaction, already paid, and are null.
35
+ *
36
+ * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
37
+ */
38
+ export declare function mapPaymentType({ notificationType, subtype }: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null;
23
39
  //#endregion
24
40
  //# sourceMappingURL=mapper.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP"}
1
+ {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP;;iBAoCc;EACf;EACA;;;;;;;;;;;;wBAac,iBACd,kBACA,WACC,wBAAwB,QAAQ"}
@@ -30,6 +30,27 @@ function mapCancellationDetails(source) {
30
30
  default: return null;
31
31
  }
32
32
  }
33
+ /**
34
+ * The payment a subscription notification's transaction is, if it is one: the first purchase
35
+ * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
36
+ * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
37
+ * full and starts a new period at once, refunding what is left of the old one separately, so it is
38
+ * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
39
+ * and the like carry the current transaction, already paid, and are null.
40
+ *
41
+ * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
42
+ */
43
+ function mapPaymentType({ notificationType, subtype }) {
44
+ switch (notificationType) {
45
+ case "SUBSCRIBED": return "SUBSCRIPTION_START";
46
+ case "DID_RENEW": return "SUBSCRIPTION_RENEWAL";
47
+ case "DID_CHANGE_RENEWAL_PREF": return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
48
+ case "OFFER_REDEEMED":
49
+ if (subtype === "INITIAL_BUY" || subtype === "RESUBSCRIBE") return "SUBSCRIPTION_START";
50
+ return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
51
+ default: return null;
52
+ }
53
+ }
33
54
  /** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */
34
55
  function expirationIntentSubtype(intent) {
35
56
  switch (intent) {
@@ -41,6 +62,6 @@ function expirationIntentSubtype(intent) {
41
62
  }
42
63
  }
43
64
  //#endregion
44
- export { mapCancellationDetails };
65
+ export { mapCancellationDetails, mapPaymentType };
45
66
 
46
67
  //# sourceMappingURL=mapper.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
1
+ {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails, PaymentType } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The App Store Server Notification fields that say whether a notification is a payment. */\nexport interface AppStorePaymentSource {\n notificationType?: string | null;\n subtype?: string | null;\n}\n\n/**\n * The payment a subscription notification's transaction is, if it is one: the first purchase\n * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal\n * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in\n * full and starts a new period at once, refunding what is left of the old one separately, so it is\n * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change\n * and the like carry the current transaction, already paid, and are null.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n */\nexport function mapPaymentType({\n notificationType,\n subtype,\n}: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null {\n switch (notificationType) {\n case 'SUBSCRIBED':\n return 'SUBSCRIPTION_START';\n case 'DID_RENEW':\n return 'SUBSCRIPTION_RENEWAL';\n case 'DID_CHANGE_RENEWAL_PREF':\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n case 'OFFER_REDEEMED':\n if (subtype === 'INITIAL_BUY' || subtype === 'RESUBSCRIBE') return 'SUBSCRIPTION_START';\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;;;;;;;;;;AAkBA,SAAgB,eAAe,EAC7B,kBACA,WACiE;CACjE,QAAQ,kBAAR;EACE,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,2BACH,OAAO,YAAY,YAAY,yBAAyB;EAC1D,KAAK;GACH,IAAI,YAAY,iBAAiB,YAAY,eAAe,OAAO;GACnE,OAAO,YAAY,YAAY,yBAAyB;EAC1D,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
@@ -10,4 +10,5 @@ exports.VoidedPurchaseProductType = require_google_play_real_time_developer_noti
10
10
  exports.VoidedPurchaseRefundType = require_google_play_real_time_developer_notification.VoidedPurchaseRefundType;
11
11
  exports.mapCanceledAt = require_google_play_mapper.mapCanceledAt;
12
12
  exports.mapCancellationDetails = require_google_play_mapper.mapCancellationDetails;
13
+ exports.mapPaymentType = require_google_play_mapper.mapPaymentType;
13
14
  exports.mapSubscriptionStatus = require_google_play_mapper.mapSubscriptionStatus;
@@ -1,4 +1,4 @@
1
1
  import { GooglePlayConfig } from "./config.cjs";
2
2
  import { DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlaySubscriptionState, OneTimeProductNotification, OneTimeProductNotificationType, PubsubMessage, SubscriptionNotification, SubscriptionNotificationType, TestNotification, VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType } from "./real-time-developer-notification.cjs";
3
- import { CanceledStateContext, mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus } from "./mapper.cjs";
4
- export { type CanceledStateContext, type DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, type GooglePlaySubscriptionState, type OneTimeProductNotification, OneTimeProductNotificationType, type PubsubMessage, type SubscriptionNotification, SubscriptionNotificationType, type TestNotification, type VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus };
3
+ import { CanceledStateContext, mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus } from "./mapper.cjs";
4
+ export { type CanceledStateContext, type DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, type GooglePlaySubscriptionState, type OneTimeProductNotification, OneTimeProductNotificationType, type PubsubMessage, type SubscriptionNotification, SubscriptionNotificationType, type TestNotification, type VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus };
@@ -1,4 +1,4 @@
1
1
  import { GooglePlayConfig } from "./config.mjs";
2
2
  import { DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlaySubscriptionState, OneTimeProductNotification, OneTimeProductNotificationType, PubsubMessage, SubscriptionNotification, SubscriptionNotificationType, TestNotification, VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType } from "./real-time-developer-notification.mjs";
3
- import { CanceledStateContext, mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus } from "./mapper.mjs";
4
- export { type CanceledStateContext, type DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, type GooglePlaySubscriptionState, type OneTimeProductNotification, OneTimeProductNotificationType, type PubsubMessage, type SubscriptionNotification, SubscriptionNotificationType, type TestNotification, type VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus };
3
+ import { CanceledStateContext, mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus } from "./mapper.mjs";
4
+ export { type CanceledStateContext, type DeveloperNotification, GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, type GooglePlaySubscriptionState, type OneTimeProductNotification, OneTimeProductNotificationType, type PubsubMessage, type SubscriptionNotification, SubscriptionNotificationType, type TestNotification, type VoidedPurchaseNotification, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus };
@@ -1,4 +1,4 @@
1
1
  import { GooglePlayConfig } from "./config.mjs";
2
2
  import { GOOGLE_PLAY_SUBSCRIPTION_STATES, OneTimeProductNotificationType, SubscriptionNotificationType, VoidedPurchaseProductType, VoidedPurchaseRefundType } from "./real-time-developer-notification.mjs";
3
- import { mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus } from "./mapper.mjs";
4
- export { GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, OneTimeProductNotificationType, SubscriptionNotificationType, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus };
3
+ import { mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus } from "./mapper.mjs";
4
+ export { GOOGLE_PLAY_SUBSCRIPTION_STATES, GooglePlayConfig, OneTimeProductNotificationType, SubscriptionNotificationType, VoidedPurchaseProductType, VoidedPurchaseRefundType, mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus };
@@ -53,6 +53,17 @@ function mapCancellationDetails(context) {
53
53
  return null;
54
54
  }
55
55
  /**
56
+ * What a paid Play subscription order was for, by its order id. Play numbers a subscription's
57
+ * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal
58
+ * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new
59
+ * purchase with an order id of its own, so it reads as a start, like any first purchase.
60
+ *
61
+ * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)
62
+ */
63
+ function mapPaymentType(orderId) {
64
+ return orderId.includes("..") ? "SUBSCRIPTION_RENEWAL" : "SUBSCRIPTION_START";
65
+ }
66
+ /**
56
67
  * When the subscription was cancelled. Only a user cancellation carries the time; for the others
57
68
  * the caller falls back to when it learnt of it.
58
69
  */
@@ -62,6 +73,7 @@ function mapCanceledAt(context) {
62
73
  //#endregion
63
74
  exports.mapCanceledAt = mapCanceledAt;
64
75
  exports.mapCancellationDetails = mapCancellationDetails;
76
+ exports.mapPaymentType = mapPaymentType;
65
77
  exports.mapSubscriptionStatus = mapSubscriptionStatus;
66
78
 
67
79
  //# sourceMappingURL=mapper.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/google-play/mapper.ts"],"sourcesContent":["import type { CancellationDetails, SubscriptionStatus } from '../subscription/index';\nimport type { GooglePlaySubscriptionState } from './real-time-developer-notification';\n\nexport function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus {\n switch (state) {\n case 'SUBSCRIPTION_STATE_PENDING':\n return 'PENDING';\n case 'SUBSCRIPTION_STATE_ACTIVE':\n return 'ACTIVE';\n case 'SUBSCRIPTION_STATE_PAUSED':\n return 'PAUSED';\n case 'SUBSCRIPTION_STATE_IN_GRACE_PERIOD':\n return 'IN_GRACE_PERIOD';\n case 'SUBSCRIPTION_STATE_ON_HOLD':\n return 'ON_HOLD';\n case 'SUBSCRIPTION_STATE_CANCELED':\n return 'CANCELED';\n case 'SUBSCRIPTION_STATE_EXPIRED':\n return 'EXPIRED';\n case 'SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED':\n return 'PENDING_PURCHASE_CANCELED';\n default:\n throw new Error(`Unknown Google Play subscription state: ${state}`);\n }\n}\n\n/** Play's cancel-survey answers in the shared vocabulary. */\nconst SURVEY_FEEDBACK: Record<string, CancellationDetails['feedback']> = {\n CANCEL_SURVEY_REASON_NOT_ENOUGH_USAGE: 'unused',\n CANCEL_SURVEY_REASON_TECHNICAL_ISSUES: 'low_quality',\n CANCEL_SURVEY_REASON_COST_RELATED: 'too_expensive',\n CANCEL_SURVEY_REASON_FOUND_BETTER_APP: 'switched_service',\n CANCEL_SURVEY_REASON_OTHERS: 'other',\n};\n\n/**\n * `canceledStateContext` of `purchases.subscriptionsv2.get`, declared structurally so this\n * module does not depend on the Play Developer API client.\n */\nexport interface CanceledStateContext {\n userInitiatedCancellation?: {\n cancelTime?: string | null;\n cancelSurveyResult?: { reason?: string | null; reasonUserInput?: string | null } | null;\n } | null;\n systemInitiatedCancellation?: object | null;\n developerInitiatedCancellation?: object | null;\n replacementCancellation?: object | null;\n}\n\n/**\n * Why a Play subscription stopped renewing. The Play Store asks the user for a reason on cancel\n * (and for free text when it is \"Other\"), which comes back in `cancelSurveyResult`. Null while\n * the subscription renews, and for a replacement: that is the linked purchase taking over, not a\n * cancellation to report.\n */\nexport function mapCancellationDetails(\n context: CanceledStateContext | null | undefined\n): CancellationDetails | null {\n if (!context) return null;\n if (context.userInitiatedCancellation) {\n const survey = context.userInitiatedCancellation.cancelSurveyResult;\n return {\n initiator: 'user',\n reason: survey?.reason ?? null,\n feedback: (survey?.reason && SURVEY_FEEDBACK[survey.reason]) || null,\n comment: survey?.reasonUserInput ?? null,\n };\n }\n if (context.systemInitiatedCancellation) {\n return { initiator: 'system', reason: 'SYSTEM_INITIATED', feedback: null, comment: null };\n }\n if (context.developerInitiatedCancellation) {\n return { initiator: 'developer', reason: 'DEVELOPER_INITIATED', feedback: null, comment: null };\n }\n return null;\n}\n\n/**\n * When the subscription was cancelled. Only a user cancellation carries the time; for the others\n * the caller falls back to when it learnt of it.\n */\nexport function mapCanceledAt(context: CanceledStateContext | null | undefined): string | null {\n return context?.userInitiatedCancellation?.cancelTime ?? null;\n}\n"],"mappings":";;AAGA,SAAgB,sBAAsB,OAAwD;CAC5F,QAAQ,OAAR;EACE,KAAK,8BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,sCACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,+BACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,gDACH,OAAO;EACT,SACE,MAAM,IAAI,MAAM,2CAA2C,OAAO;CACtE;AACF;;AAGA,MAAM,kBAAmE;CACvE,uCAAuC;CACvC,uCAAuC;CACvC,mCAAmC;CACnC,uCAAuC;CACvC,6BAA6B;AAC/B;;;;;;;AAsBA,SAAgB,uBACd,SAC4B;CAC5B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,2BAA2B;EACrC,MAAM,SAAS,QAAQ,0BAA0B;EACjD,OAAO;GACL,WAAW;GACX,QAAQ,QAAQ,UAAU;GAC1B,UAAW,QAAQ,UAAU,gBAAgB,OAAO,WAAY;GAChE,SAAS,QAAQ,mBAAmB;EACtC;CACF;CACA,IAAI,QAAQ,6BACV,OAAO;EAAE,WAAW;EAAU,QAAQ;EAAoB,UAAU;EAAM,SAAS;CAAK;CAE1F,IAAI,QAAQ,gCACV,OAAO;EAAE,WAAW;EAAa,QAAQ;EAAuB,UAAU;EAAM,SAAS;CAAK;CAEhG,OAAO;AACT;;;;;AAMA,SAAgB,cAAc,SAAiE;CAC7F,OAAO,SAAS,2BAA2B,cAAc;AAC3D"}
1
+ {"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/google-play/mapper.ts"],"sourcesContent":["import type { CancellationDetails, PaymentType, SubscriptionStatus } from '../subscription/index';\nimport type { GooglePlaySubscriptionState } from './real-time-developer-notification';\n\nexport function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus {\n switch (state) {\n case 'SUBSCRIPTION_STATE_PENDING':\n return 'PENDING';\n case 'SUBSCRIPTION_STATE_ACTIVE':\n return 'ACTIVE';\n case 'SUBSCRIPTION_STATE_PAUSED':\n return 'PAUSED';\n case 'SUBSCRIPTION_STATE_IN_GRACE_PERIOD':\n return 'IN_GRACE_PERIOD';\n case 'SUBSCRIPTION_STATE_ON_HOLD':\n return 'ON_HOLD';\n case 'SUBSCRIPTION_STATE_CANCELED':\n return 'CANCELED';\n case 'SUBSCRIPTION_STATE_EXPIRED':\n return 'EXPIRED';\n case 'SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED':\n return 'PENDING_PURCHASE_CANCELED';\n default:\n throw new Error(`Unknown Google Play subscription state: ${state}`);\n }\n}\n\n/** Play's cancel-survey answers in the shared vocabulary. */\nconst SURVEY_FEEDBACK: Record<string, CancellationDetails['feedback']> = {\n CANCEL_SURVEY_REASON_NOT_ENOUGH_USAGE: 'unused',\n CANCEL_SURVEY_REASON_TECHNICAL_ISSUES: 'low_quality',\n CANCEL_SURVEY_REASON_COST_RELATED: 'too_expensive',\n CANCEL_SURVEY_REASON_FOUND_BETTER_APP: 'switched_service',\n CANCEL_SURVEY_REASON_OTHERS: 'other',\n};\n\n/**\n * `canceledStateContext` of `purchases.subscriptionsv2.get`, declared structurally so this\n * module does not depend on the Play Developer API client.\n */\nexport interface CanceledStateContext {\n userInitiatedCancellation?: {\n cancelTime?: string | null;\n cancelSurveyResult?: { reason?: string | null; reasonUserInput?: string | null } | null;\n } | null;\n systemInitiatedCancellation?: object | null;\n developerInitiatedCancellation?: object | null;\n replacementCancellation?: object | null;\n}\n\n/**\n * Why a Play subscription stopped renewing. The Play Store asks the user for a reason on cancel\n * (and for free text when it is \"Other\"), which comes back in `cancelSurveyResult`. Null while\n * the subscription renews, and for a replacement: that is the linked purchase taking over, not a\n * cancellation to report.\n */\nexport function mapCancellationDetails(\n context: CanceledStateContext | null | undefined\n): CancellationDetails | null {\n if (!context) return null;\n if (context.userInitiatedCancellation) {\n const survey = context.userInitiatedCancellation.cancelSurveyResult;\n return {\n initiator: 'user',\n reason: survey?.reason ?? null,\n feedback: (survey?.reason && SURVEY_FEEDBACK[survey.reason]) || null,\n comment: survey?.reasonUserInput ?? null,\n };\n }\n if (context.systemInitiatedCancellation) {\n return { initiator: 'system', reason: 'SYSTEM_INITIATED', feedback: null, comment: null };\n }\n if (context.developerInitiatedCancellation) {\n return { initiator: 'developer', reason: 'DEVELOPER_INITIATED', feedback: null, comment: null };\n }\n return null;\n}\n\n/**\n * What a paid Play subscription order was for, by its order id. Play numbers a subscription's\n * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal\n * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new\n * purchase with an order id of its own, so it reads as a start, like any first purchase.\n *\n * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)\n */\nexport function mapPaymentType(orderId: string): Exclude<PaymentType, 'ONE_TIME'> {\n return orderId.includes('..') ? 'SUBSCRIPTION_RENEWAL' : 'SUBSCRIPTION_START';\n}\n\n/**\n * When the subscription was cancelled. Only a user cancellation carries the time; for the others\n * the caller falls back to when it learnt of it.\n */\nexport function mapCanceledAt(context: CanceledStateContext | null | undefined): string | null {\n return context?.userInitiatedCancellation?.cancelTime ?? null;\n}\n"],"mappings":";;AAGA,SAAgB,sBAAsB,OAAwD;CAC5F,QAAQ,OAAR;EACE,KAAK,8BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,sCACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,+BACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,gDACH,OAAO;EACT,SACE,MAAM,IAAI,MAAM,2CAA2C,OAAO;CACtE;AACF;;AAGA,MAAM,kBAAmE;CACvE,uCAAuC;CACvC,uCAAuC;CACvC,mCAAmC;CACnC,uCAAuC;CACvC,6BAA6B;AAC/B;;;;;;;AAsBA,SAAgB,uBACd,SAC4B;CAC5B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,2BAA2B;EACrC,MAAM,SAAS,QAAQ,0BAA0B;EACjD,OAAO;GACL,WAAW;GACX,QAAQ,QAAQ,UAAU;GAC1B,UAAW,QAAQ,UAAU,gBAAgB,OAAO,WAAY;GAChE,SAAS,QAAQ,mBAAmB;EACtC;CACF;CACA,IAAI,QAAQ,6BACV,OAAO;EAAE,WAAW;EAAU,QAAQ;EAAoB,UAAU;EAAM,SAAS;CAAK;CAE1F,IAAI,QAAQ,gCACV,OAAO;EAAE,WAAW;EAAa,QAAQ;EAAuB,UAAU;EAAM,SAAS;CAAK;CAEhG,OAAO;AACT;;;;;;;;;AAUA,SAAgB,eAAe,SAAmD;CAChF,OAAO,QAAQ,SAAS,IAAI,IAAI,yBAAyB;AAC3D;;;;;AAMA,SAAgB,cAAc,SAAiE;CAC7F,OAAO,SAAS,2BAA2B,cAAc;AAC3D"}
@@ -1,4 +1,4 @@
1
- import { CancellationDetails, SubscriptionStatus } from "../subscription/index.cjs";
1
+ import { CancellationDetails, PaymentType, SubscriptionStatus } from "../subscription/index.cjs";
2
2
  import { GooglePlaySubscriptionState } from "./real-time-developer-notification.cjs";
3
3
  //#region src/google-play/mapper.d.ts
4
4
  export declare function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus;
@@ -25,6 +25,15 @@ export interface CanceledStateContext {
25
25
  * cancellation to report.
26
26
  */
27
27
  export declare function mapCancellationDetails(context: CanceledStateContext | null | undefined): CancellationDetails | null;
28
+ /**
29
+ * What a paid Play subscription order was for, by its order id. Play numbers a subscription's
30
+ * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal
31
+ * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new
32
+ * purchase with an order id of its own, so it reads as a start, like any first purchase.
33
+ *
34
+ * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)
35
+ */
36
+ export declare function mapPaymentType(orderId: string): Exclude<PaymentType, 'ONE_TIME'>;
28
37
  /**
29
38
  * When the subscription was cancelled. Only a user cancellation carries the time; for the others
30
39
  * the caller falls back to when it learnt of it.
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/google-play/mapper.ts"],"mappings":";;;wBAGgB,sBAAsB,OAAO,8BAA8B;;;;;iBAoC1D;EACf;IACE;IACA;MAAuB;MAAwB;;;EAEjD;EACA;EACA;;;;;;;;wBASc,uBACd,SAAS,0CACR;;;;;wBAwBa,cAAc,SAAS"}
1
+ {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/google-play/mapper.ts"],"mappings":";;;wBAGgB,sBAAsB,OAAO,8BAA8B;;;;;iBAoC1D;EACf;IACE;IACA;MAAuB;MAAwB;;;EAEjD;EACA;EACA;;;;;;;;wBASc,uBACd,SAAS,0CACR;;;;;;;;;wBA4Ba,eAAe,kBAAkB,QAAQ;;;;;wBAQzC,cAAc,SAAS"}
@@ -1,4 +1,4 @@
1
- import { CancellationDetails, SubscriptionStatus } from "../subscription/index.mjs";
1
+ import { CancellationDetails, PaymentType, SubscriptionStatus } from "../subscription/index.mjs";
2
2
  import { GooglePlaySubscriptionState } from "./real-time-developer-notification.mjs";
3
3
  //#region src/google-play/mapper.d.ts
4
4
  export declare function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus;
@@ -25,6 +25,15 @@ export interface CanceledStateContext {
25
25
  * cancellation to report.
26
26
  */
27
27
  export declare function mapCancellationDetails(context: CanceledStateContext | null | undefined): CancellationDetails | null;
28
+ /**
29
+ * What a paid Play subscription order was for, by its order id. Play numbers a subscription's
30
+ * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal
31
+ * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new
32
+ * purchase with an order id of its own, so it reads as a start, like any first purchase.
33
+ *
34
+ * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)
35
+ */
36
+ export declare function mapPaymentType(orderId: string): Exclude<PaymentType, 'ONE_TIME'>;
28
37
  /**
29
38
  * When the subscription was cancelled. Only a user cancellation carries the time; for the others
30
39
  * the caller falls back to when it learnt of it.
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/google-play/mapper.ts"],"mappings":";;;wBAGgB,sBAAsB,OAAO,8BAA8B;;;;;iBAoC1D;EACf;IACE;IACA;MAAuB;MAAwB;;;EAEjD;EACA;EACA;;;;;;;;wBASc,uBACd,SAAS,0CACR;;;;;wBAwBa,cAAc,SAAS"}
1
+ {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/google-play/mapper.ts"],"mappings":";;;wBAGgB,sBAAsB,OAAO,8BAA8B;;;;;iBAoC1D;EACf;IACE;IACA;MAAuB;MAAwB;;;EAEjD;EACA;EACA;;;;;;;;wBASc,uBACd,SAAS,0CACR;;;;;;;;;wBA4Ba,eAAe,kBAAkB,QAAQ;;;;;wBAQzC,cAAc,SAAS"}
@@ -52,6 +52,17 @@ function mapCancellationDetails(context) {
52
52
  return null;
53
53
  }
54
54
  /**
55
+ * What a paid Play subscription order was for, by its order id. Play numbers a subscription's
56
+ * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal
57
+ * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new
58
+ * purchase with an order id of its own, so it reads as a start, like any first purchase.
59
+ *
60
+ * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)
61
+ */
62
+ function mapPaymentType(orderId) {
63
+ return orderId.includes("..") ? "SUBSCRIPTION_RENEWAL" : "SUBSCRIPTION_START";
64
+ }
65
+ /**
55
66
  * When the subscription was cancelled. Only a user cancellation carries the time; for the others
56
67
  * the caller falls back to when it learnt of it.
57
68
  */
@@ -59,6 +70,6 @@ function mapCanceledAt(context) {
59
70
  return context?.userInitiatedCancellation?.cancelTime ?? null;
60
71
  }
61
72
  //#endregion
62
- export { mapCanceledAt, mapCancellationDetails, mapSubscriptionStatus };
73
+ export { mapCanceledAt, mapCancellationDetails, mapPaymentType, mapSubscriptionStatus };
63
74
 
64
75
  //# sourceMappingURL=mapper.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/google-play/mapper.ts"],"sourcesContent":["import type { CancellationDetails, SubscriptionStatus } from '../subscription/index';\nimport type { GooglePlaySubscriptionState } from './real-time-developer-notification';\n\nexport function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus {\n switch (state) {\n case 'SUBSCRIPTION_STATE_PENDING':\n return 'PENDING';\n case 'SUBSCRIPTION_STATE_ACTIVE':\n return 'ACTIVE';\n case 'SUBSCRIPTION_STATE_PAUSED':\n return 'PAUSED';\n case 'SUBSCRIPTION_STATE_IN_GRACE_PERIOD':\n return 'IN_GRACE_PERIOD';\n case 'SUBSCRIPTION_STATE_ON_HOLD':\n return 'ON_HOLD';\n case 'SUBSCRIPTION_STATE_CANCELED':\n return 'CANCELED';\n case 'SUBSCRIPTION_STATE_EXPIRED':\n return 'EXPIRED';\n case 'SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED':\n return 'PENDING_PURCHASE_CANCELED';\n default:\n throw new Error(`Unknown Google Play subscription state: ${state}`);\n }\n}\n\n/** Play's cancel-survey answers in the shared vocabulary. */\nconst SURVEY_FEEDBACK: Record<string, CancellationDetails['feedback']> = {\n CANCEL_SURVEY_REASON_NOT_ENOUGH_USAGE: 'unused',\n CANCEL_SURVEY_REASON_TECHNICAL_ISSUES: 'low_quality',\n CANCEL_SURVEY_REASON_COST_RELATED: 'too_expensive',\n CANCEL_SURVEY_REASON_FOUND_BETTER_APP: 'switched_service',\n CANCEL_SURVEY_REASON_OTHERS: 'other',\n};\n\n/**\n * `canceledStateContext` of `purchases.subscriptionsv2.get`, declared structurally so this\n * module does not depend on the Play Developer API client.\n */\nexport interface CanceledStateContext {\n userInitiatedCancellation?: {\n cancelTime?: string | null;\n cancelSurveyResult?: { reason?: string | null; reasonUserInput?: string | null } | null;\n } | null;\n systemInitiatedCancellation?: object | null;\n developerInitiatedCancellation?: object | null;\n replacementCancellation?: object | null;\n}\n\n/**\n * Why a Play subscription stopped renewing. The Play Store asks the user for a reason on cancel\n * (and for free text when it is \"Other\"), which comes back in `cancelSurveyResult`. Null while\n * the subscription renews, and for a replacement: that is the linked purchase taking over, not a\n * cancellation to report.\n */\nexport function mapCancellationDetails(\n context: CanceledStateContext | null | undefined\n): CancellationDetails | null {\n if (!context) return null;\n if (context.userInitiatedCancellation) {\n const survey = context.userInitiatedCancellation.cancelSurveyResult;\n return {\n initiator: 'user',\n reason: survey?.reason ?? null,\n feedback: (survey?.reason && SURVEY_FEEDBACK[survey.reason]) || null,\n comment: survey?.reasonUserInput ?? null,\n };\n }\n if (context.systemInitiatedCancellation) {\n return { initiator: 'system', reason: 'SYSTEM_INITIATED', feedback: null, comment: null };\n }\n if (context.developerInitiatedCancellation) {\n return { initiator: 'developer', reason: 'DEVELOPER_INITIATED', feedback: null, comment: null };\n }\n return null;\n}\n\n/**\n * When the subscription was cancelled. Only a user cancellation carries the time; for the others\n * the caller falls back to when it learnt of it.\n */\nexport function mapCanceledAt(context: CanceledStateContext | null | undefined): string | null {\n return context?.userInitiatedCancellation?.cancelTime ?? null;\n}\n"],"mappings":";AAGA,SAAgB,sBAAsB,OAAwD;CAC5F,QAAQ,OAAR;EACE,KAAK,8BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,sCACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,+BACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,gDACH,OAAO;EACT,SACE,MAAM,IAAI,MAAM,2CAA2C,OAAO;CACtE;AACF;;AAGA,MAAM,kBAAmE;CACvE,uCAAuC;CACvC,uCAAuC;CACvC,mCAAmC;CACnC,uCAAuC;CACvC,6BAA6B;AAC/B;;;;;;;AAsBA,SAAgB,uBACd,SAC4B;CAC5B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,2BAA2B;EACrC,MAAM,SAAS,QAAQ,0BAA0B;EACjD,OAAO;GACL,WAAW;GACX,QAAQ,QAAQ,UAAU;GAC1B,UAAW,QAAQ,UAAU,gBAAgB,OAAO,WAAY;GAChE,SAAS,QAAQ,mBAAmB;EACtC;CACF;CACA,IAAI,QAAQ,6BACV,OAAO;EAAE,WAAW;EAAU,QAAQ;EAAoB,UAAU;EAAM,SAAS;CAAK;CAE1F,IAAI,QAAQ,gCACV,OAAO;EAAE,WAAW;EAAa,QAAQ;EAAuB,UAAU;EAAM,SAAS;CAAK;CAEhG,OAAO;AACT;;;;;AAMA,SAAgB,cAAc,SAAiE;CAC7F,OAAO,SAAS,2BAA2B,cAAc;AAC3D"}
1
+ {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/google-play/mapper.ts"],"sourcesContent":["import type { CancellationDetails, PaymentType, SubscriptionStatus } from '../subscription/index';\nimport type { GooglePlaySubscriptionState } from './real-time-developer-notification';\n\nexport function mapSubscriptionStatus(state: GooglePlaySubscriptionState): SubscriptionStatus {\n switch (state) {\n case 'SUBSCRIPTION_STATE_PENDING':\n return 'PENDING';\n case 'SUBSCRIPTION_STATE_ACTIVE':\n return 'ACTIVE';\n case 'SUBSCRIPTION_STATE_PAUSED':\n return 'PAUSED';\n case 'SUBSCRIPTION_STATE_IN_GRACE_PERIOD':\n return 'IN_GRACE_PERIOD';\n case 'SUBSCRIPTION_STATE_ON_HOLD':\n return 'ON_HOLD';\n case 'SUBSCRIPTION_STATE_CANCELED':\n return 'CANCELED';\n case 'SUBSCRIPTION_STATE_EXPIRED':\n return 'EXPIRED';\n case 'SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED':\n return 'PENDING_PURCHASE_CANCELED';\n default:\n throw new Error(`Unknown Google Play subscription state: ${state}`);\n }\n}\n\n/** Play's cancel-survey answers in the shared vocabulary. */\nconst SURVEY_FEEDBACK: Record<string, CancellationDetails['feedback']> = {\n CANCEL_SURVEY_REASON_NOT_ENOUGH_USAGE: 'unused',\n CANCEL_SURVEY_REASON_TECHNICAL_ISSUES: 'low_quality',\n CANCEL_SURVEY_REASON_COST_RELATED: 'too_expensive',\n CANCEL_SURVEY_REASON_FOUND_BETTER_APP: 'switched_service',\n CANCEL_SURVEY_REASON_OTHERS: 'other',\n};\n\n/**\n * `canceledStateContext` of `purchases.subscriptionsv2.get`, declared structurally so this\n * module does not depend on the Play Developer API client.\n */\nexport interface CanceledStateContext {\n userInitiatedCancellation?: {\n cancelTime?: string | null;\n cancelSurveyResult?: { reason?: string | null; reasonUserInput?: string | null } | null;\n } | null;\n systemInitiatedCancellation?: object | null;\n developerInitiatedCancellation?: object | null;\n replacementCancellation?: object | null;\n}\n\n/**\n * Why a Play subscription stopped renewing. The Play Store asks the user for a reason on cancel\n * (and for free text when it is \"Other\"), which comes back in `cancelSurveyResult`. Null while\n * the subscription renews, and for a replacement: that is the linked purchase taking over, not a\n * cancellation to report.\n */\nexport function mapCancellationDetails(\n context: CanceledStateContext | null | undefined\n): CancellationDetails | null {\n if (!context) return null;\n if (context.userInitiatedCancellation) {\n const survey = context.userInitiatedCancellation.cancelSurveyResult;\n return {\n initiator: 'user',\n reason: survey?.reason ?? null,\n feedback: (survey?.reason && SURVEY_FEEDBACK[survey.reason]) || null,\n comment: survey?.reasonUserInput ?? null,\n };\n }\n if (context.systemInitiatedCancellation) {\n return { initiator: 'system', reason: 'SYSTEM_INITIATED', feedback: null, comment: null };\n }\n if (context.developerInitiatedCancellation) {\n return { initiator: 'developer', reason: 'DEVELOPER_INITIATED', feedback: null, comment: null };\n }\n return null;\n}\n\n/**\n * What a paid Play subscription order was for, by its order id. Play numbers a subscription's\n * orders from the first purchase: the initial order is `GPA.1234-5678-9012-34567`, each renewal\n * the same id with `..0`, `..1`, ... appended. A plan change through the Play Store is a new\n * purchase with an order id of its own, so it reads as a start, like any first purchase.\n *\n * ref: [Order ID](https://developer.android.com/google/play/billing/subscriptions#renewals)\n */\nexport function mapPaymentType(orderId: string): Exclude<PaymentType, 'ONE_TIME'> {\n return orderId.includes('..') ? 'SUBSCRIPTION_RENEWAL' : 'SUBSCRIPTION_START';\n}\n\n/**\n * When the subscription was cancelled. Only a user cancellation carries the time; for the others\n * the caller falls back to when it learnt of it.\n */\nexport function mapCanceledAt(context: CanceledStateContext | null | undefined): string | null {\n return context?.userInitiatedCancellation?.cancelTime ?? null;\n}\n"],"mappings":";AAGA,SAAgB,sBAAsB,OAAwD;CAC5F,QAAQ,OAAR;EACE,KAAK,8BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,6BACH,OAAO;EACT,KAAK,sCACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,+BACH,OAAO;EACT,KAAK,8BACH,OAAO;EACT,KAAK,gDACH,OAAO;EACT,SACE,MAAM,IAAI,MAAM,2CAA2C,OAAO;CACtE;AACF;;AAGA,MAAM,kBAAmE;CACvE,uCAAuC;CACvC,uCAAuC;CACvC,mCAAmC;CACnC,uCAAuC;CACvC,6BAA6B;AAC/B;;;;;;;AAsBA,SAAgB,uBACd,SAC4B;CAC5B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,2BAA2B;EACrC,MAAM,SAAS,QAAQ,0BAA0B;EACjD,OAAO;GACL,WAAW;GACX,QAAQ,QAAQ,UAAU;GAC1B,UAAW,QAAQ,UAAU,gBAAgB,OAAO,WAAY;GAChE,SAAS,QAAQ,mBAAmB;EACtC;CACF;CACA,IAAI,QAAQ,6BACV,OAAO;EAAE,WAAW;EAAU,QAAQ;EAAoB,UAAU;EAAM,SAAS;CAAK;CAE1F,IAAI,QAAQ,gCACV,OAAO;EAAE,WAAW;EAAa,QAAQ;EAAuB,UAAU;EAAM,SAAS;CAAK;CAEhG,OAAO;AACT;;;;;;;;;AAUA,SAAgB,eAAe,SAAmD;CAChF,OAAO,QAAQ,SAAS,IAAI,IAAI,yBAAyB;AAC3D;;;;;AAMA,SAAgB,cAAc,SAAiE;CAC7F,OAAO,SAAS,2BAA2B,cAAc;AAC3D"}
@@ -105,6 +105,15 @@ var StripeConfig = class StripeConfig {
105
105
  (0, _shware_utils.invariant)(product, `Product not found for ${productId}`);
106
106
  return product.plan === null ? "payment" : "subscription";
107
107
  };
108
+ /**
109
+ * Whether the config declares `productId` and sells it as a subscription, so `getPlan` and
110
+ * `getBillingPeriod` hold for it. False, not a throw, for an id it does not know: a product that
111
+ * left the config (renamed, retired) or one named by metadata set outside the app.
112
+ */
113
+ isSubscription = (productId) => {
114
+ const product = this.products.get(productId);
115
+ return product !== void 0 && product.plan !== null;
116
+ };
108
117
  /** The price new checkouts are created with: the default, or its test twin in test mode. */
109
118
  getPriceId = (productId) => {
110
119
  const product = this.products.get(productId);
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs","names":[],"sources":["../../src/stripe/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport type { Metadata } from '../types';\n\nexport type PriceId = `price_${string}`;\n\n/**\n * Which Stripe object space the config addresses. Test and live prices are\n * disjoint, so a config is built for exactly one of them. Derive it from the\n * secret key (`sk_test_` vs `sk_live_`) rather than from the deployment\n * environment: the key is what actually decides which space API calls hit.\n */\nexport type StripeMode = 'live' | 'test';\n\nconst addMethod = Symbol('addMethod');\n\nclass Product<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends PriceId = never,\n> {\n readonly id: string;\n readonly config: StripeConfig<NS, PE, BP, PL, PI>;\n readonly plan: PE | null;\n readonly billingPeriod: BP | null;\n readonly prices: Map<PriceId, Metadata>;\n\n defaultPriceId: PriceId | null;\n\n constructor(\n config: StripeConfig<NS, PE, BP, PL, PI>,\n id: string,\n plan: PE | null = null,\n billingPeriod: BP | null = null\n ) {\n this.id = id;\n this.config = config;\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.prices = new Map();\n this.defaultPriceId = null;\n }\n\n /**\n * Non-default prices stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * create new checkouts with a legacy price unless that is intentional.\n */\n price = <K extends PriceId>(\n priceId: K extends I ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Product<NS, PE, BP, PL, PI, I | K> => {\n invariant(!this.prices.has(priceId), `Duplicate price ${priceId}`);\n this.prices.set(priceId, metadata);\n return this as Product<NS, PE, BP, PL, PI, I | K>;\n };\n\n /**\n * `test` is the sandbox twin of the default price. In test mode it is what\n * checkouts are created with, and it resolves to the default price's\n * entitlement, so credits are declared once and cannot drift between the\n * two modes. Legacy prices need no twin: a sandbox holds no grandfathered\n * subscribers. Live mode ignores it, so a test price reaching a live config\n * still fails as an unknown price instead of being granted an entitlement.\n */\n default = (defaultPriceId: I, twin?: { test: PriceId }) => {\n const metadata = this.prices.get(defaultPriceId);\n invariant(metadata, `Default price ${defaultPriceId} is not declared`);\n if (this.config.mode === 'test') {\n invariant(twin, `Test price not declared for default ${defaultPriceId} of ${this.id}`);\n invariant(!this.prices.has(twin.test), `Duplicate price ${twin.test}`);\n this.prices.set(twin.test, metadata);\n this.defaultPriceId = twin.test;\n } else {\n this.defaultPriceId = defaultPriceId;\n }\n this.config[addMethod](this);\n return this.config;\n };\n}\n\ntype Options = {\n mode: StripeMode;\n returnUrl: string;\n cancelUrl: string;\n successUrl: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n cancellationCouponId: string;\n allowPromotionCodes: boolean;\n};\n\nexport class StripeConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n private readonly products: Map<string, Product<NS, PE, BP, PL, PI>> = new Map();\n\n public readonly mode: StripeMode;\n public returnUrl: string;\n public cancelUrl: string;\n public successUrl: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n public cancellationCouponId: string;\n public allowPromotionCodes: boolean;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (product: Product<NS, PE, BP, PL, PI>) => {\n invariant(!this.products.has(product.id), `Duplicate product ${product.id}`);\n // A Stripe price belongs to one Stripe product, and getProductId must have one answer.\n for (const existing of this.products.values()) {\n for (const priceId of product.prices.keys()) {\n invariant(!existing.prices.has(priceId), `Duplicate price ${priceId} on ${existing.id}`);\n }\n }\n if (product.plan !== null) {\n for (const existing of this.products.values()) {\n invariant(\n existing.plan !== product.plan || existing.billingPeriod !== product.billingPeriod,\n `Duplicate plan ${product.plan}:${product.billingPeriod}`\n );\n }\n }\n this.products.set(product.id, product);\n return this;\n };\n\n private constructor(options: Options) {\n this.mode = options.mode;\n this.returnUrl = options.returnUrl;\n this.cancelUrl = options.cancelUrl;\n this.successUrl = options.successUrl;\n this.cancellationCouponId = options.cancellationCouponId;\n this.allowPromotionCodes = options.allowPromotionCodes;\n }\n\n static create = <NS extends Lowercase<string>, PE extends string, BP extends string = string>(\n options: Options\n ) => {\n return new StripeConfig<NS, PE, BP>(options);\n };\n\n /**\n * One-time products take no extra args; subscriptions require both plan and billingPeriod.\n * A plan+period pair may be declared on only one product — version its prices in that chain.\n */\n product: {\n <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K\n ): Product<NS, PE, BP, PL, K | PI>;\n <K extends `${NS}.${Lowercase<string>}`, L extends PE, P extends BP>(\n productId: K extends PI ? never : K,\n plan: L,\n billingPeriod: `${L}:${P}` extends PL ? never : P\n ): Product<NS, PE, BP, `${L}:${P}` | PL, K | PI>;\n } = (productId: string, plan: PE | null = null, billingPeriod: BP | null = null) =>\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- widens to the overloads' accumulated generics\n new Product(this, productId, plan, billingPeriod) as never;\n\n getPlan = (productId: string): PE => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.plan, `Product ${productId} is not a subscription`);\n return product.plan;\n };\n\n getBillingPeriod = (productId: string): BP => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.billingPeriod, `Product ${productId} is not a subscription`);\n return product.billingPeriod;\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n return product.plan === null ? 'payment' : 'subscription';\n };\n\n /** The price new checkouts are created with: the default, or its test twin in test mode. */\n getPriceId = (productId: string): PriceId => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return product.defaultPriceId;\n };\n\n /**\n * The product that declares `priceId` (a legacy price or the test twin included), or null for a\n * price this config does not know: one set up by hand in the dashboard, or a test price reaching\n * a live config. Reading the product from the price a Stripe object charged, rather than from\n * metadata that may already name the next plan, is what an invoice should be credited by.\n */\n getProductId = (priceId: string): string | null => {\n for (const product of this.products.values()) {\n if (product.prices.has(priceId as PriceId)) return product.id;\n }\n return null;\n };\n\n getCreditAmount = (productId: string, priceId?: string): number => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n if (priceId) {\n const price = product.prices.get(priceId as PriceId);\n invariant(price, `Price not found for ${priceId}`);\n return price.credits;\n }\n\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return product.prices.get(product.defaultPriceId)?.credits ?? 0;\n };\n\n private readonly getCreditExpiresIn = (productId: string, priceId?: string): number => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n if (priceId) {\n const price = product.prices.get(priceId as PriceId);\n invariant(price, `Price not found for ${priceId}`);\n return ms(price.expiresIn);\n }\n\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return ms(product.prices.get(product.defaultPriceId)?.expiresIn ?? '0');\n };\n\n getCreditExpiresAt = (productId: string, priceId?: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId, priceId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.cjs","names":[],"sources":["../../src/stripe/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport type { Metadata } from '../types';\n\nexport type PriceId = `price_${string}`;\n\n/**\n * Which Stripe object space the config addresses. Test and live prices are\n * disjoint, so a config is built for exactly one of them. Derive it from the\n * secret key (`sk_test_` vs `sk_live_`) rather than from the deployment\n * environment: the key is what actually decides which space API calls hit.\n */\nexport type StripeMode = 'live' | 'test';\n\nconst addMethod = Symbol('addMethod');\n\nclass Product<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends PriceId = never,\n> {\n readonly id: string;\n readonly config: StripeConfig<NS, PE, BP, PL, PI>;\n readonly plan: PE | null;\n readonly billingPeriod: BP | null;\n readonly prices: Map<PriceId, Metadata>;\n\n defaultPriceId: PriceId | null;\n\n constructor(\n config: StripeConfig<NS, PE, BP, PL, PI>,\n id: string,\n plan: PE | null = null,\n billingPeriod: BP | null = null\n ) {\n this.id = id;\n this.config = config;\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.prices = new Map();\n this.defaultPriceId = null;\n }\n\n /**\n * Non-default prices stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * create new checkouts with a legacy price unless that is intentional.\n */\n price = <K extends PriceId>(\n priceId: K extends I ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Product<NS, PE, BP, PL, PI, I | K> => {\n invariant(!this.prices.has(priceId), `Duplicate price ${priceId}`);\n this.prices.set(priceId, metadata);\n return this as Product<NS, PE, BP, PL, PI, I | K>;\n };\n\n /**\n * `test` is the sandbox twin of the default price. In test mode it is what\n * checkouts are created with, and it resolves to the default price's\n * entitlement, so credits are declared once and cannot drift between the\n * two modes. Legacy prices need no twin: a sandbox holds no grandfathered\n * subscribers. Live mode ignores it, so a test price reaching a live config\n * still fails as an unknown price instead of being granted an entitlement.\n */\n default = (defaultPriceId: I, twin?: { test: PriceId }) => {\n const metadata = this.prices.get(defaultPriceId);\n invariant(metadata, `Default price ${defaultPriceId} is not declared`);\n if (this.config.mode === 'test') {\n invariant(twin, `Test price not declared for default ${defaultPriceId} of ${this.id}`);\n invariant(!this.prices.has(twin.test), `Duplicate price ${twin.test}`);\n this.prices.set(twin.test, metadata);\n this.defaultPriceId = twin.test;\n } else {\n this.defaultPriceId = defaultPriceId;\n }\n this.config[addMethod](this);\n return this.config;\n };\n}\n\ntype Options = {\n mode: StripeMode;\n returnUrl: string;\n cancelUrl: string;\n successUrl: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n cancellationCouponId: string;\n allowPromotionCodes: boolean;\n};\n\nexport class StripeConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n private readonly products: Map<string, Product<NS, PE, BP, PL, PI>> = new Map();\n\n public readonly mode: StripeMode;\n public returnUrl: string;\n public cancelUrl: string;\n public successUrl: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n public cancellationCouponId: string;\n public allowPromotionCodes: boolean;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (product: Product<NS, PE, BP, PL, PI>) => {\n invariant(!this.products.has(product.id), `Duplicate product ${product.id}`);\n // A Stripe price belongs to one Stripe product, and getProductId must have one answer.\n for (const existing of this.products.values()) {\n for (const priceId of product.prices.keys()) {\n invariant(!existing.prices.has(priceId), `Duplicate price ${priceId} on ${existing.id}`);\n }\n }\n if (product.plan !== null) {\n for (const existing of this.products.values()) {\n invariant(\n existing.plan !== product.plan || existing.billingPeriod !== product.billingPeriod,\n `Duplicate plan ${product.plan}:${product.billingPeriod}`\n );\n }\n }\n this.products.set(product.id, product);\n return this;\n };\n\n private constructor(options: Options) {\n this.mode = options.mode;\n this.returnUrl = options.returnUrl;\n this.cancelUrl = options.cancelUrl;\n this.successUrl = options.successUrl;\n this.cancellationCouponId = options.cancellationCouponId;\n this.allowPromotionCodes = options.allowPromotionCodes;\n }\n\n static create = <NS extends Lowercase<string>, PE extends string, BP extends string = string>(\n options: Options\n ) => {\n return new StripeConfig<NS, PE, BP>(options);\n };\n\n /**\n * One-time products take no extra args; subscriptions require both plan and billingPeriod.\n * A plan+period pair may be declared on only one product — version its prices in that chain.\n */\n product: {\n <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K\n ): Product<NS, PE, BP, PL, K | PI>;\n <K extends `${NS}.${Lowercase<string>}`, L extends PE, P extends BP>(\n productId: K extends PI ? never : K,\n plan: L,\n billingPeriod: `${L}:${P}` extends PL ? never : P\n ): Product<NS, PE, BP, `${L}:${P}` | PL, K | PI>;\n } = (productId: string, plan: PE | null = null, billingPeriod: BP | null = null) =>\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- widens to the overloads' accumulated generics\n new Product(this, productId, plan, billingPeriod) as never;\n\n getPlan = (productId: string): PE => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.plan, `Product ${productId} is not a subscription`);\n return product.plan;\n };\n\n getBillingPeriod = (productId: string): BP => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.billingPeriod, `Product ${productId} is not a subscription`);\n return product.billingPeriod;\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n return product.plan === null ? 'payment' : 'subscription';\n };\n\n /**\n * Whether the config declares `productId` and sells it as a subscription, so `getPlan` and\n * `getBillingPeriod` hold for it. False, not a throw, for an id it does not know: a product that\n * left the config (renamed, retired) or one named by metadata set outside the app.\n */\n isSubscription = (productId: string): boolean => {\n const product = this.products.get(productId);\n return product !== undefined && product.plan !== null;\n };\n\n /** The price new checkouts are created with: the default, or its test twin in test mode. */\n getPriceId = (productId: string): PriceId => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return product.defaultPriceId;\n };\n\n /**\n * The product that declares `priceId` (a legacy price or the test twin included), or null for a\n * price this config does not know: one set up by hand in the dashboard, or a test price reaching\n * a live config. Reading the product from the price a Stripe object charged, rather than from\n * metadata that may already name the next plan, is what an invoice should be credited by.\n */\n getProductId = (priceId: string): string | null => {\n for (const product of this.products.values()) {\n if (product.prices.has(priceId as PriceId)) return product.id;\n }\n return null;\n };\n\n getCreditAmount = (productId: string, priceId?: string): number => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n if (priceId) {\n const price = product.prices.get(priceId as PriceId);\n invariant(price, `Price not found for ${priceId}`);\n return price.credits;\n }\n\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return product.prices.get(product.defaultPriceId)?.credits ?? 0;\n };\n\n private readonly getCreditExpiresIn = (productId: string, priceId?: string): number => {\n const product = this.products.get(productId);\n invariant(product, `Product not found for ${productId}`);\n if (priceId) {\n const price = product.prices.get(priceId as PriceId);\n invariant(price, `Price not found for ${priceId}`);\n return ms(price.expiresIn);\n }\n\n invariant(product.defaultPriceId, `Default price not found for product ${productId}`);\n return ms(product.prices.get(product.defaultPriceId)?.expiresIn ?? '0');\n };\n\n getCreditExpiresAt = (productId: string, priceId?: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId, priceId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n"],"mappings":"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"}
@@ -66,6 +66,12 @@ export declare class StripeConfig<NS extends Lowercase<string> = Lowercase<strin
66
66
  getPlan: (productId: string) => PE;
67
67
  getBillingPeriod: (productId: string) => BP;
68
68
  getMode: (productId: string) => 'payment' | 'subscription';
69
+ /**
70
+ * Whether the config declares `productId` and sells it as a subscription, so `getPlan` and
71
+ * `getBillingPeriod` hold for it. False, not a throw, for an id it does not know: a product that
72
+ * left the config (renamed, retired) or one named by metadata set outside the app.
73
+ */
74
+ isSubscription: (productId: string) => boolean;
69
75
  /** The price new checkouts are created with: the default, or its test twin in test mode. */
70
76
  getPriceId: (productId: string) => PriceId;
71
77
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.cts","names":[],"sources":["../../src/stripe/config.ts"],"mappings":";;YAIY;;;;;;;YAQA;cAEN;cAEA,QACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA,UAAU;WAED;WACA,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI;WACrC,MAAM;WACN,eAAe;WACf,QAAQ,IAAI,SAAS;EAE9B,gBAAgB;EAEhB,YACE,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI,KACrC,YACA,OAAM,WACN,gBAAe;;;;;;EAejB,QAAS,UAAU,SAAO,SACf,UAAU,YAAY,GAAC,WACtB,aACT,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;;;;;;;;;EAcnC,UAAO,gBAAoB,GAAC;IAAW,MAAM;QAAS,aAAA,IAAA,IAAA,IAAA,IAAA;;KAgBnD;EACH,MAAM;EACN;EACA;EACA;EACA;EACA;;qBAGW,aACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;mBAEiB;WAED,MAAM;EACf;EACA;EACA;EACA;EACA;MAEH;GAMH,aAAU,SAAa,QAAQ,IAAI,IAAI,IAAI,IAAI;UAoBzC;SASA,SAAU,aAAW,mBAAmB,qBAAmB,8BAA0B,SACjF,YAAO,aAAA,MAAA,MAAA;;;;;EASlB;KACG,aAAa,MAAM,qBAClB,WAAW,UAAU,aAAa,IACjC,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI;KAC9B,aAAa,MAAM,qBAAqB,UAAU,IAAI,UAAU,IAC/D,WAAW,UAAU,aAAa,GAClC,MAAM,GACN,kBAAkB,KAAK,YAAY,aAAa,IAC/C,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,IAAI,IAAI;;EAK/C,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;;EAOP,aAAU,sBAAwB;;;;;;;EAalC,eAAY;EAOZ,kBAAe,mBAAqB;mBAanB;EAajB,qBAAkB,mBAAqB"}
1
+ {"version":3,"file":"config.d.cts","names":[],"sources":["../../src/stripe/config.ts"],"mappings":";;YAIY;;;;;;;YAQA;cAEN;cAEA,QACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA,UAAU;WAED;WACA,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI;WACrC,MAAM;WACN,eAAe;WACf,QAAQ,IAAI,SAAS;EAE9B,gBAAgB;EAEhB,YACE,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI,KACrC,YACA,OAAM,WACN,gBAAe;;;;;;EAejB,QAAS,UAAU,SAAO,SACf,UAAU,YAAY,GAAC,WACtB,aACT,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;;;;;;;;;EAcnC,UAAO,gBAAoB,GAAC;IAAW,MAAM;QAAS,aAAA,IAAA,IAAA,IAAA,IAAA;;KAgBnD;EACH,MAAM;EACN;EACA;EACA;EACA;EACA;;qBAGW,aACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;mBAEiB;WAED,MAAM;EACf;EACA;EACA;EACA;EACA;MAEH;GAMH,aAAU,SAAa,QAAQ,IAAI,IAAI,IAAI,IAAI;UAoBzC;SASA,SAAU,aAAW,mBAAmB,qBAAmB,8BAA0B,SACjF,YAAO,aAAA,MAAA,MAAA;;;;;EASlB;KACG,aAAa,MAAM,qBAClB,WAAW,UAAU,aAAa,IACjC,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI;KAC9B,aAAa,MAAM,qBAAqB,UAAU,IAAI,UAAU,IAC/D,WAAW,UAAU,aAAa,GAClC,MAAM,GACN,kBAAkB,KAAK,YAAY,aAAa,IAC/C,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,IAAI,IAAI;;EAK/C,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;;;;;;EAWP,iBAAc;;EAMd,aAAU,sBAAwB;;;;;;;EAalC,eAAY;EAOZ,kBAAe,mBAAqB;mBAanB;EAajB,qBAAkB,mBAAqB"}
@@ -66,6 +66,12 @@ export declare class StripeConfig<NS extends Lowercase<string> = Lowercase<strin
66
66
  getPlan: (productId: string) => PE;
67
67
  getBillingPeriod: (productId: string) => BP;
68
68
  getMode: (productId: string) => 'payment' | 'subscription';
69
+ /**
70
+ * Whether the config declares `productId` and sells it as a subscription, so `getPlan` and
71
+ * `getBillingPeriod` hold for it. False, not a throw, for an id it does not know: a product that
72
+ * left the config (renamed, retired) or one named by metadata set outside the app.
73
+ */
74
+ isSubscription: (productId: string) => boolean;
69
75
  /** The price new checkouts are created with: the default, or its test twin in test mode. */
70
76
  getPriceId: (productId: string) => PriceId;
71
77
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/stripe/config.ts"],"mappings":";;YAIY;;;;;;;YAQA;cAEN;cAEA,QACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA,UAAU;WAED;WACA,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI;WACrC,MAAM;WACN,eAAe;WACf,QAAQ,IAAI,SAAS;EAE9B,gBAAgB;EAEhB,YACE,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI,KACrC,YACA,OAAM,WACN,gBAAe;;;;;;EAejB,QAAS,UAAU,SAAO,SACf,UAAU,YAAY,GAAC,WACtB,aACT,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;;;;;;;;;EAcnC,UAAO,gBAAoB,GAAC;IAAW,MAAM;QAAS,aAAA,IAAA,IAAA,IAAA,IAAA;;KAgBnD;EACH,MAAM;EACN;EACA;EACA;EACA;EACA;;qBAGW,aACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;mBAEiB;WAED,MAAM;EACf;EACA;EACA;EACA;EACA;MAEH;GAMH,aAAU,SAAa,QAAQ,IAAI,IAAI,IAAI,IAAI;UAoBzC;SASA,SAAU,aAAW,mBAAmB,qBAAmB,8BAA0B,SACjF,YAAO,aAAA,MAAA,MAAA;;;;;EASlB;KACG,aAAa,MAAM,qBAClB,WAAW,UAAU,aAAa,IACjC,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI;KAC9B,aAAa,MAAM,qBAAqB,UAAU,IAAI,UAAU,IAC/D,WAAW,UAAU,aAAa,GAClC,MAAM,GACN,kBAAkB,KAAK,YAAY,aAAa,IAC/C,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,IAAI,IAAI;;EAK/C,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;;EAOP,aAAU,sBAAwB;;;;;;;EAalC,eAAY;EAOZ,kBAAe,mBAAqB;mBAanB;EAajB,qBAAkB,mBAAqB"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/stripe/config.ts"],"mappings":";;YAIY;;;;;;;YAQA;cAEN;cAEA,QACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA,UAAU;WAED;WACA,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI;WACrC,MAAM;WACN,eAAe;WACf,QAAQ,IAAI,SAAS;EAE9B,gBAAgB;EAEhB,YACE,QAAQ,aAAa,IAAI,IAAI,IAAI,IAAI,KACrC,YACA,OAAM,WACN,gBAAe;;;;;;EAejB,QAAS,UAAU,SAAO,SACf,UAAU,YAAY,GAAC,WACtB,aACT,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;;;;;;;;;EAcnC,UAAO,gBAAoB,GAAC;IAAW,MAAM;QAAS,aAAA,IAAA,IAAA,IAAA,IAAA;;KAgBnD;EACH,MAAM;EACN;EACA;EACA;EACA;EACA;;qBAGW,aACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;mBAEiB;WAED,MAAM;EACf;EACA;EACA;EACA;EACA;MAEH;GAMH,aAAU,SAAa,QAAQ,IAAI,IAAI,IAAI,IAAI;UAoBzC;SASA,SAAU,aAAW,mBAAmB,qBAAmB,8BAA0B,SACjF,YAAO,aAAA,MAAA,MAAA;;;;;EASlB;KACG,aAAa,MAAM,qBAClB,WAAW,UAAU,aAAa,IACjC,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI;KAC9B,aAAa,MAAM,qBAAqB,UAAU,IAAI,UAAU,IAC/D,WAAW,UAAU,aAAa,GAClC,MAAM,GACN,kBAAkB,KAAK,YAAY,aAAa,IAC/C,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,IAAI,IAAI;;EAK/C,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;;;;;;EAWP,iBAAc;;EAMd,aAAU,sBAAwB;;;;;;;EAalC,eAAY;EAOZ,kBAAe,mBAAqB;mBAanB;EAajB,qBAAkB,mBAAqB"}
@@ -102,6 +102,15 @@ var StripeConfig = class StripeConfig {
102
102
  invariant(product, `Product not found for ${productId}`);
103
103
  return product.plan === null ? "payment" : "subscription";
104
104
  };
105
+ /**
106
+ * Whether the config declares `productId` and sells it as a subscription, so `getPlan` and
107
+ * `getBillingPeriod` hold for it. False, not a throw, for an id it does not know: a product that
108
+ * left the config (renamed, retired) or one named by metadata set outside the app.
109
+ */
110
+ isSubscription = (productId) => {
111
+ const product = this.products.get(productId);
112
+ return product !== void 0 && product.plan !== null;
113
+ };
105
114
  /** The price new checkouts are created with: the default, or its test twin in test mode. */
106
115
  getPriceId = (productId) => {
107
116
  const product = this.products.get(productId);