@feelflow/ffid-sdk 5.6.0 → 5.7.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.
@@ -842,7 +842,7 @@ function createProfileMethods(deps) {
842
842
  }
843
843
 
844
844
  // src/client/version-check.ts
845
- var SDK_VERSION = "5.6.0";
845
+ var SDK_VERSION = "5.7.0";
846
846
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
847
847
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
848
848
  function sdkHeaders() {
@@ -2247,7 +2247,7 @@ function isExpired(isoTimestamp, nowMs) {
2247
2247
  return parsed < nowMs;
2248
2248
  }
2249
2249
  function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2250
- const { status, currentPeriodEnd, trialEnd, pastDueSince } = input;
2250
+ const { status, currentPeriodEnd, trialEnd, pastDueSince, cancelAtPeriodEnd = false } = input;
2251
2251
  switch (status) {
2252
2252
  case "trialing": {
2253
2253
  const relevantEnd = trialEnd ?? currentPeriodEnd;
@@ -2256,7 +2256,8 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2256
2256
  }
2257
2257
  case "active": {
2258
2258
  const expired = isExpired(currentPeriodEnd, nowMs);
2259
- return expired === true ? "expired" : "active";
2259
+ if (expired === true) return "expired";
2260
+ return cancelAtPeriodEnd ? "active_canceling" : "active";
2260
2261
  }
2261
2262
  case "past_due": {
2262
2263
  const baselineIso = pastDueSince ?? currentPeriodEnd;
@@ -2285,6 +2286,7 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2285
2286
  // src/subscriptions/userinfo-helpers.ts
2286
2287
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2287
2288
  "active",
2289
+ "active_canceling",
2288
2290
  "past_due_grace"
2289
2291
  ];
2290
2292
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2338,6 +2340,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2338
2340
  var DEFAULT_ALLOW_GRACE = true;
2339
2341
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2340
2342
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2343
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2344
+ blocked: "blocked",
2345
+ canceled: "canceled",
2346
+ expired: "expired",
2347
+ trial_expired: "trial_expired"
2348
+ };
2341
2349
  function resolveServiceAccessDenialReason(params, allowGrace) {
2342
2350
  if (params.hasAccess) {
2343
2351
  return null;
@@ -2348,10 +2356,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2348
2356
  if (params.isGrace && !allowGrace) {
2349
2357
  return "grace_disallowed";
2350
2358
  }
2351
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2359
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2352
2360
  return "blocked";
2353
2361
  }
2354
- return params.effectiveStatus;
2362
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2355
2363
  }
2356
2364
  function toServiceAccessDecision(response, allowGrace) {
2357
2365
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -3522,6 +3530,7 @@ function FFIDOrganizationSwitcher({
3522
3530
  }
3523
3531
  var ACCESS_GRANTING_STATUSES = [
3524
3532
  "active",
3533
+ "active_canceling",
3525
3534
  "past_due_grace"
3526
3535
  ];
3527
3536
  var BLOCKING_STATUSES = [
@@ -3558,7 +3567,8 @@ function useSubscription() {
3558
3567
  // のため `computeEffectiveStatusFromSession` 内で currentPeriodEnd を
3559
3568
  // fallback に使う (Stripe は period end 近辺で past_due にする)。
3560
3569
  // Canonical な verdict が必要な consumer は `/ext/check` を直接呼ぶ。
3561
- pastDueSince: void 0
3570
+ pastDueSince: void 0,
3571
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd
3562
3572
  });
3563
3573
  }, [subscription]);
3564
3574
  const isGrace = effectiveStatus === "past_due_grace";
@@ -840,7 +840,7 @@ function createProfileMethods(deps) {
840
840
  }
841
841
 
842
842
  // src/client/version-check.ts
843
- var SDK_VERSION = "5.6.0";
843
+ var SDK_VERSION = "5.7.0";
844
844
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
845
845
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
846
846
  function sdkHeaders() {
@@ -2245,7 +2245,7 @@ function isExpired(isoTimestamp, nowMs) {
2245
2245
  return parsed < nowMs;
2246
2246
  }
2247
2247
  function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2248
- const { status, currentPeriodEnd, trialEnd, pastDueSince } = input;
2248
+ const { status, currentPeriodEnd, trialEnd, pastDueSince, cancelAtPeriodEnd = false } = input;
2249
2249
  switch (status) {
2250
2250
  case "trialing": {
2251
2251
  const relevantEnd = trialEnd ?? currentPeriodEnd;
@@ -2254,7 +2254,8 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2254
2254
  }
2255
2255
  case "active": {
2256
2256
  const expired = isExpired(currentPeriodEnd, nowMs);
2257
- return expired === true ? "expired" : "active";
2257
+ if (expired === true) return "expired";
2258
+ return cancelAtPeriodEnd ? "active_canceling" : "active";
2258
2259
  }
2259
2260
  case "past_due": {
2260
2261
  const baselineIso = pastDueSince ?? currentPeriodEnd;
@@ -2283,6 +2284,7 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2283
2284
  // src/subscriptions/userinfo-helpers.ts
2284
2285
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2285
2286
  "active",
2287
+ "active_canceling",
2286
2288
  "past_due_grace"
2287
2289
  ];
2288
2290
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2336,6 +2338,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2336
2338
  var DEFAULT_ALLOW_GRACE = true;
2337
2339
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2338
2340
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2341
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2342
+ blocked: "blocked",
2343
+ canceled: "canceled",
2344
+ expired: "expired",
2345
+ trial_expired: "trial_expired"
2346
+ };
2339
2347
  function resolveServiceAccessDenialReason(params, allowGrace) {
2340
2348
  if (params.hasAccess) {
2341
2349
  return null;
@@ -2346,10 +2354,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2346
2354
  if (params.isGrace && !allowGrace) {
2347
2355
  return "grace_disallowed";
2348
2356
  }
2349
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2357
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2350
2358
  return "blocked";
2351
2359
  }
2352
- return params.effectiveStatus;
2360
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2353
2361
  }
2354
2362
  function toServiceAccessDecision(response, allowGrace) {
2355
2363
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -3520,6 +3528,7 @@ function FFIDOrganizationSwitcher({
3520
3528
  }
3521
3529
  var ACCESS_GRANTING_STATUSES = [
3522
3530
  "active",
3531
+ "active_canceling",
3523
3532
  "past_due_grace"
3524
3533
  ];
3525
3534
  var BLOCKING_STATUSES = [
@@ -3556,7 +3565,8 @@ function useSubscription() {
3556
3565
  // のため `computeEffectiveStatusFromSession` 内で currentPeriodEnd を
3557
3566
  // fallback に使う (Stripe は period end 近辺で past_due にする)。
3558
3567
  // Canonical な verdict が必要な consumer は `/ext/check` を直接呼ぶ。
3559
- pastDueSince: void 0
3568
+ pastDueSince: void 0,
3569
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd
3560
3570
  });
3561
3571
  }, [subscription]);
3562
3572
  const isGrace = effectiveStatus === "past_due_grace";
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkWGSEZSZI_cjs = require('../chunk-WGSEZSZI.cjs');
3
+ var chunkFXIDKFXQ_cjs = require('../chunk-FXIDKFXQ.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunkWGSEZSZI_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunkWGSEZSZI_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunkWGSEZSZI_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunkWGSEZSZI_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunkWGSEZSZI_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunkWGSEZSZI_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunkWGSEZSZI_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDUserMenu; }
34
34
  });
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-_oO5sHjX.cjs';
1
+ export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-EFp_ZgK9.cjs';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1,3 +1,3 @@
1
- export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-_oO5sHjX.js';
1
+ export { M as FFIDAnnouncementBadge, al as FFIDAnnouncementBadgeClassNames, am as FFIDAnnouncementBadgeProps, N as FFIDAnnouncementList, an as FFIDAnnouncementListClassNames, ao as FFIDAnnouncementListProps, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a4 as FFIDLoginButton, ap as FFIDLoginButtonProps, a7 as FFIDOrganizationSwitcher, aq as FFIDOrganizationSwitcherClassNames, ar as FFIDOrganizationSwitcherProps, ac as FFIDSubscriptionBadge, as as FFIDSubscriptionBadgeClassNames, at as FFIDSubscriptionBadgeProps, ae as FFIDUserMenu, au as FFIDUserMenuClassNames, av as FFIDUserMenuProps } from '../index-EFp_ZgK9.js';
2
2
  import 'react/jsx-runtime';
3
3
  import 'react';
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-4SJKXEYB.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-YAJN36SD.js';
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-5g_Bg6Ey.js';
1
+ import { E as EffectiveSubscriptionStatus } from './types-s3y11uT6.js';
2
2
 
3
3
  /**
4
4
  * Inquiry types exposed by the FFID SDK.
@@ -724,6 +724,16 @@ interface FFIDSubscription {
724
724
  currentPeriodEnd: string | null;
725
725
  /** Trial end date (null if not a trial or no trial_end set) */
726
726
  trialEnd: string | null;
727
+ /**
728
+ * Whether the subscription is scheduled to cancel at the end of the current
729
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
730
+ * compatibility with FFID backends that pre-date #3460 — when absent,
731
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
732
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
733
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
734
+ * + reactivation CTA without losing access.
735
+ */
736
+ cancelAtPeriodEnd?: boolean;
727
737
  /** Service seat model (available when sourced from OAuth userinfo) */
728
738
  seatModel?: FFIDSeatModel | undefined;
729
739
  /** Member role in the resolved organization context */
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-5g_Bg6Ey.cjs';
1
+ import { E as EffectiveSubscriptionStatus } from './types-s3y11uT6.cjs';
2
2
 
3
3
  /**
4
4
  * Inquiry types exposed by the FFID SDK.
@@ -724,6 +724,16 @@ interface FFIDSubscription {
724
724
  currentPeriodEnd: string | null;
725
725
  /** Trial end date (null if not a trial or no trial_end set) */
726
726
  trialEnd: string | null;
727
+ /**
728
+ * Whether the subscription is scheduled to cancel at the end of the current
729
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
730
+ * compatibility with FFID backends that pre-date #3460 — when absent,
731
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
732
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
733
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
734
+ * + reactivation CTA without losing access.
735
+ */
736
+ cancelAtPeriodEnd?: boolean;
727
737
  /** Service seat model (available when sourced from OAuth userinfo) */
728
738
  seatModel?: FFIDSeatModel | undefined;
729
739
  /** Member role in the resolved organization context */
@@ -41,6 +41,10 @@ interface FFIDCacheConfig {
41
41
  * branch on a single value:
42
42
  *
43
43
  * - `active` — normal paying subscription; grant full access.
44
+ * - `active_canceling` — paying subscription with `cancel_at_period_end=true`.
45
+ * Access stays live until `currentPeriodEnd`; callers SHOULD surface a
46
+ * "cancellation scheduled" badge and a reactivation CTA. Session-only
47
+ * refinement of `active` — see the asymmetry note below.
44
48
  * - `past_due_grace` — payment failed but FFID is still retrying. Service
45
49
  * SHOULD keep access and surface a recovery banner.
46
50
  * - `blocked` — payment dunning exhausted (`past_due` over grace,
@@ -53,10 +57,27 @@ interface FFIDCacheConfig {
53
57
  * This is the FFID-internal runtime-expired case (see
54
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
55
59
  *
60
+ * **SDK / server asymmetry — `active_canceling` is session-only (#3460).**
61
+ * The backend `EffectiveSubscriptionStatus` in
62
+ * `src/lib/common/subscription-helpers.ts` does NOT currently include
63
+ * `active_canceling`; cancellation-scheduled subscriptions surface as plain
64
+ * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
65
+ * consumers that branch on the server verdict will not see the new literal.
66
+ * The SDK enriches `computeEffectiveStatusFromSession` with
67
+ * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
68
+ * #3460 onward — older backends omit the field and the helper defaults to
69
+ * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
70
+ * can surface a "cancellation scheduled" badge without waiting for an
71
+ * `/ext/check` round-trip. The asymmetry is safe because the new literal is
72
+ * access-granting (no security regression on the userinfo path); when the
73
+ * server's `EffectiveSubscriptionStatus` is later extended to emit
74
+ * `active_canceling`, this docstring MUST be updated to drop the
75
+ * session-only note.
76
+ *
56
77
  * @see /api/v1/subscriptions/ext/check
57
78
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
58
79
  */
59
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
80
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
60
81
 
61
82
  /**
62
83
  * Canonical service-access types for subscription lifecycle decisions.
@@ -230,6 +251,16 @@ interface FFIDSubscription {
230
251
  currentPeriodEnd: string | null;
231
252
  /** Trial end date (null if not a trial or no trial_end set) */
232
253
  trialEnd: string | null;
254
+ /**
255
+ * Whether the subscription is scheduled to cancel at the end of the current
256
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
257
+ * compatibility with FFID backends that pre-date #3460 — when absent,
258
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
259
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
260
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
261
+ * + reactivation CTA without losing access.
262
+ */
263
+ cancelAtPeriodEnd?: boolean;
233
264
  /** Service seat model (available when sourced from OAuth userinfo) */
234
265
  seatModel?: FFIDSeatModel | undefined;
235
266
  /** Member role in the resolved organization context */
@@ -41,6 +41,10 @@ interface FFIDCacheConfig {
41
41
  * branch on a single value:
42
42
  *
43
43
  * - `active` — normal paying subscription; grant full access.
44
+ * - `active_canceling` — paying subscription with `cancel_at_period_end=true`.
45
+ * Access stays live until `currentPeriodEnd`; callers SHOULD surface a
46
+ * "cancellation scheduled" badge and a reactivation CTA. Session-only
47
+ * refinement of `active` — see the asymmetry note below.
44
48
  * - `past_due_grace` — payment failed but FFID is still retrying. Service
45
49
  * SHOULD keep access and surface a recovery banner.
46
50
  * - `blocked` — payment dunning exhausted (`past_due` over grace,
@@ -53,10 +57,27 @@ interface FFIDCacheConfig {
53
57
  * This is the FFID-internal runtime-expired case (see
54
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
55
59
  *
60
+ * **SDK / server asymmetry — `active_canceling` is session-only (#3460).**
61
+ * The backend `EffectiveSubscriptionStatus` in
62
+ * `src/lib/common/subscription-helpers.ts` does NOT currently include
63
+ * `active_canceling`; cancellation-scheduled subscriptions surface as plain
64
+ * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
65
+ * consumers that branch on the server verdict will not see the new literal.
66
+ * The SDK enriches `computeEffectiveStatusFromSession` with
67
+ * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
68
+ * #3460 onward — older backends omit the field and the helper defaults to
69
+ * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
70
+ * can surface a "cancellation scheduled" badge without waiting for an
71
+ * `/ext/check` round-trip. The asymmetry is safe because the new literal is
72
+ * access-granting (no security regression on the userinfo path); when the
73
+ * server's `EffectiveSubscriptionStatus` is later extended to emit
74
+ * `active_canceling`, this docstring MUST be updated to drop the
75
+ * session-only note.
76
+ *
56
77
  * @see /api/v1/subscriptions/ext/check
57
78
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
58
79
  */
59
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
80
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
60
81
 
61
82
  /**
62
83
  * Canonical service-access types for subscription lifecycle decisions.
@@ -230,6 +251,16 @@ interface FFIDSubscription {
230
251
  currentPeriodEnd: string | null;
231
252
  /** Trial end date (null if not a trial or no trial_end set) */
232
253
  trialEnd: string | null;
254
+ /**
255
+ * Whether the subscription is scheduled to cancel at the end of the current
256
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
257
+ * compatibility with FFID backends that pre-date #3460 — when absent,
258
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
259
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
260
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
261
+ * + reactivation CTA without losing access.
262
+ */
263
+ cancelAtPeriodEnd?: boolean;
233
264
  /** Service seat model (available when sourced from OAuth userinfo) */
234
265
  seatModel?: FFIDSeatModel | undefined;
235
266
  /** Member role in the resolved organization context */
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWGSEZSZI_cjs = require('./chunk-WGSEZSZI.cjs');
3
+ var chunkFXIDKFXQ_cjs = require('./chunk-FXIDKFXQ.cjs');
4
4
  var chunkQ5SZVLNB_cjs = require('./chunk-Q5SZVLNB.cjs');
5
5
  var react = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
@@ -54,8 +54,8 @@ function defaultRedirect(url) {
54
54
  }
55
55
  function useRequireActiveSubscription(options) {
56
56
  const { redirectTo, allowGrace = true, onRedirect } = options;
57
- const { isLoading, error } = chunkWGSEZSZI_cjs.useFFIDContext();
58
- const { effectiveStatus, isBlocked, isGrace } = chunkWGSEZSZI_cjs.useSubscription();
57
+ const { isLoading, error } = chunkFXIDKFXQ_cjs.useFFIDContext();
58
+ const { effectiveStatus, isBlocked, isGrace } = chunkFXIDKFXQ_cjs.useSubscription();
59
59
  const hasFetchError = error !== null && effectiveStatus === null;
60
60
  const shouldRedirect = !isLoading && !hasFetchError && (isBlocked || !allowGrace && isGrace || effectiveStatus === null);
61
61
  react.useEffect(() => {
@@ -76,7 +76,7 @@ function useRequireActiveSubscription(options) {
76
76
  }
77
77
  function withFFIDAuth(Component, options = {}) {
78
78
  const WrappedComponent = (props) => {
79
- const { isLoading, isAuthenticated, login } = chunkWGSEZSZI_cjs.useFFIDContext();
79
+ const { isLoading, isAuthenticated, login } = chunkFXIDKFXQ_cjs.useFFIDContext();
80
80
  const hasRedirected = react.useRef(false);
81
81
  react.useEffect(() => {
82
82
  if (!isLoading && !isAuthenticated && options.redirectToLogin && !hasRedirected.current) {
@@ -105,131 +105,131 @@ var FFID_NEWSLETTER_DISPATCH_MAX_RECIPIENTS = 1e3;
105
105
 
106
106
  Object.defineProperty(exports, "ACCESS_GRANTING_EFFECTIVE_STATUSES", {
107
107
  enumerable: true,
108
- get: function () { return chunkWGSEZSZI_cjs.ACCESS_GRANTING_EFFECTIVE_STATUSES; }
108
+ get: function () { return chunkFXIDKFXQ_cjs.ACCESS_GRANTING_EFFECTIVE_STATUSES; }
109
109
  });
110
110
  Object.defineProperty(exports, "BLOCKING_EFFECTIVE_STATUSES", {
111
111
  enumerable: true,
112
- get: function () { return chunkWGSEZSZI_cjs.BLOCKING_EFFECTIVE_STATUSES; }
112
+ get: function () { return chunkFXIDKFXQ_cjs.BLOCKING_EFFECTIVE_STATUSES; }
113
113
  });
114
114
  Object.defineProperty(exports, "DEFAULT_API_BASE_URL", {
115
115
  enumerable: true,
116
- get: function () { return chunkWGSEZSZI_cjs.DEFAULT_API_BASE_URL; }
116
+ get: function () { return chunkFXIDKFXQ_cjs.DEFAULT_API_BASE_URL; }
117
117
  });
118
118
  Object.defineProperty(exports, "DEFAULT_OAUTH_SCOPES", {
119
119
  enumerable: true,
120
- get: function () { return chunkWGSEZSZI_cjs.DEFAULT_OAUTH_SCOPES; }
120
+ get: function () { return chunkFXIDKFXQ_cjs.DEFAULT_OAUTH_SCOPES; }
121
121
  });
122
122
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
123
123
  enumerable: true,
124
- get: function () { return chunkWGSEZSZI_cjs.FFIDAnnouncementBadge; }
124
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementBadge; }
125
125
  });
126
126
  Object.defineProperty(exports, "FFIDAnnouncementList", {
127
127
  enumerable: true,
128
- get: function () { return chunkWGSEZSZI_cjs.FFIDAnnouncementList; }
128
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDAnnouncementList; }
129
129
  });
130
130
  Object.defineProperty(exports, "FFIDInquiryForm", {
131
131
  enumerable: true,
132
- get: function () { return chunkWGSEZSZI_cjs.FFIDInquiryForm; }
132
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDInquiryForm; }
133
133
  });
134
134
  Object.defineProperty(exports, "FFIDLoginButton", {
135
135
  enumerable: true,
136
- get: function () { return chunkWGSEZSZI_cjs.FFIDLoginButton; }
136
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDLoginButton; }
137
137
  });
138
138
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
139
139
  enumerable: true,
140
- get: function () { return chunkWGSEZSZI_cjs.FFIDOrganizationSwitcher; }
140
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDOrganizationSwitcher; }
141
141
  });
142
142
  Object.defineProperty(exports, "FFIDProvider", {
143
143
  enumerable: true,
144
- get: function () { return chunkWGSEZSZI_cjs.FFIDProvider; }
144
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDProvider; }
145
145
  });
146
146
  Object.defineProperty(exports, "FFIDSDKError", {
147
147
  enumerable: true,
148
- get: function () { return chunkWGSEZSZI_cjs.FFIDSDKError; }
148
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDSDKError; }
149
149
  });
150
150
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
151
151
  enumerable: true,
152
- get: function () { return chunkWGSEZSZI_cjs.FFIDSubscriptionBadge; }
152
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDSubscriptionBadge; }
153
153
  });
154
154
  Object.defineProperty(exports, "FFIDUserMenu", {
155
155
  enumerable: true,
156
- get: function () { return chunkWGSEZSZI_cjs.FFIDUserMenu; }
156
+ get: function () { return chunkFXIDKFXQ_cjs.FFIDUserMenu; }
157
157
  });
158
158
  Object.defineProperty(exports, "FFID_ANNOUNCEMENTS_ERROR_CODES", {
159
159
  enumerable: true,
160
- get: function () { return chunkWGSEZSZI_cjs.FFID_ANNOUNCEMENTS_ERROR_CODES; }
160
+ get: function () { return chunkFXIDKFXQ_cjs.FFID_ANNOUNCEMENTS_ERROR_CODES; }
161
161
  });
162
162
  Object.defineProperty(exports, "FFID_INQUIRY_CATEGORIES", {
163
163
  enumerable: true,
164
- get: function () { return chunkWGSEZSZI_cjs.FFID_INQUIRY_CATEGORIES; }
164
+ get: function () { return chunkFXIDKFXQ_cjs.FFID_INQUIRY_CATEGORIES; }
165
165
  });
166
166
  Object.defineProperty(exports, "FFID_INQUIRY_CATEGORIES_SITE_2026", {
167
167
  enumerable: true,
168
- get: function () { return chunkWGSEZSZI_cjs.FFID_INQUIRY_CATEGORIES_SITE_2026; }
168
+ get: function () { return chunkFXIDKFXQ_cjs.FFID_INQUIRY_CATEGORIES_SITE_2026; }
169
169
  });
170
170
  Object.defineProperty(exports, "computeEffectiveStatusFromSession", {
171
171
  enumerable: true,
172
- get: function () { return chunkWGSEZSZI_cjs.computeEffectiveStatusFromSession; }
172
+ get: function () { return chunkFXIDKFXQ_cjs.computeEffectiveStatusFromSession; }
173
173
  });
174
174
  Object.defineProperty(exports, "createFFIDAnnouncementsClient", {
175
175
  enumerable: true,
176
- get: function () { return chunkWGSEZSZI_cjs.createFFIDAnnouncementsClient; }
176
+ get: function () { return chunkFXIDKFXQ_cjs.createFFIDAnnouncementsClient; }
177
177
  });
178
178
  Object.defineProperty(exports, "createFFIDClient", {
179
179
  enumerable: true,
180
- get: function () { return chunkWGSEZSZI_cjs.createFFIDClient; }
180
+ get: function () { return chunkFXIDKFXQ_cjs.createFFIDClient; }
181
181
  });
182
182
  Object.defineProperty(exports, "createTokenStore", {
183
183
  enumerable: true,
184
- get: function () { return chunkWGSEZSZI_cjs.createTokenStore; }
184
+ get: function () { return chunkFXIDKFXQ_cjs.createTokenStore; }
185
185
  });
186
186
  Object.defineProperty(exports, "generateCodeChallenge", {
187
187
  enumerable: true,
188
- get: function () { return chunkWGSEZSZI_cjs.generateCodeChallenge; }
188
+ get: function () { return chunkFXIDKFXQ_cjs.generateCodeChallenge; }
189
189
  });
190
190
  Object.defineProperty(exports, "generateCodeVerifier", {
191
191
  enumerable: true,
192
- get: function () { return chunkWGSEZSZI_cjs.generateCodeVerifier; }
192
+ get: function () { return chunkFXIDKFXQ_cjs.generateCodeVerifier; }
193
193
  });
194
194
  Object.defineProperty(exports, "hasAccessFromUserinfo", {
195
195
  enumerable: true,
196
- get: function () { return chunkWGSEZSZI_cjs.hasAccessFromUserinfo; }
196
+ get: function () { return chunkFXIDKFXQ_cjs.hasAccessFromUserinfo; }
197
197
  });
198
198
  Object.defineProperty(exports, "isBlockedFromUserinfo", {
199
199
  enumerable: true,
200
- get: function () { return chunkWGSEZSZI_cjs.isBlockedFromUserinfo; }
200
+ get: function () { return chunkFXIDKFXQ_cjs.isBlockedFromUserinfo; }
201
201
  });
202
202
  Object.defineProperty(exports, "isFFIDInquiryCategorySite2026", {
203
203
  enumerable: true,
204
- get: function () { return chunkWGSEZSZI_cjs.isFFIDInquiryCategorySite2026; }
204
+ get: function () { return chunkFXIDKFXQ_cjs.isFFIDInquiryCategorySite2026; }
205
205
  });
206
206
  Object.defineProperty(exports, "normalizeRedirectUri", {
207
207
  enumerable: true,
208
- get: function () { return chunkWGSEZSZI_cjs.normalizeRedirectUri; }
208
+ get: function () { return chunkFXIDKFXQ_cjs.normalizeRedirectUri; }
209
209
  });
210
210
  Object.defineProperty(exports, "retrieveCodeVerifier", {
211
211
  enumerable: true,
212
- get: function () { return chunkWGSEZSZI_cjs.retrieveCodeVerifier; }
212
+ get: function () { return chunkFXIDKFXQ_cjs.retrieveCodeVerifier; }
213
213
  });
214
214
  Object.defineProperty(exports, "storeCodeVerifier", {
215
215
  enumerable: true,
216
- get: function () { return chunkWGSEZSZI_cjs.storeCodeVerifier; }
216
+ get: function () { return chunkFXIDKFXQ_cjs.storeCodeVerifier; }
217
217
  });
218
218
  Object.defineProperty(exports, "useFFID", {
219
219
  enumerable: true,
220
- get: function () { return chunkWGSEZSZI_cjs.useFFID; }
220
+ get: function () { return chunkFXIDKFXQ_cjs.useFFID; }
221
221
  });
222
222
  Object.defineProperty(exports, "useFFIDAnnouncements", {
223
223
  enumerable: true,
224
- get: function () { return chunkWGSEZSZI_cjs.useFFIDAnnouncements; }
224
+ get: function () { return chunkFXIDKFXQ_cjs.useFFIDAnnouncements; }
225
225
  });
226
226
  Object.defineProperty(exports, "useSubscription", {
227
227
  enumerable: true,
228
- get: function () { return chunkWGSEZSZI_cjs.useSubscription; }
228
+ get: function () { return chunkFXIDKFXQ_cjs.useSubscription; }
229
229
  });
230
230
  Object.defineProperty(exports, "withSubscription", {
231
231
  enumerable: true,
232
- get: function () { return chunkWGSEZSZI_cjs.withSubscription; }
232
+ get: function () { return chunkFXIDKFXQ_cjs.withSubscription; }
233
233
  });
234
234
  Object.defineProperty(exports, "ALL_DENIED_EXCEPT_NECESSARY", {
235
235
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FFIDSubscriptionStatus, a as FFIDConfig, b as FFIDApiResponse, c as FFIDSessionResponse, d as FFIDRedirectResult, e as FFIDError, f as FFIDSubscriptionCheckResponse, g as FFIDCheckServiceAccessParams, h as FFIDServiceAccessDecision, i as FFIDProfileCallOptions, j as FFIDUserProfile, k as FFIDUpdateUserProfileRequest, l as FFIDAnalyticsConfig, m as FFIDCreateCheckoutParams, n as FFIDCheckoutSessionResponse, o as FFIDCreatePortalParams, p as FFIDPortalSessionResponse, q as FFIDVerifyAccessTokenOptions, r as FFIDOAuthUserInfo, s as FFIDInquiryCreateParams, t as FFIDInquiryCreateResponse, u as FFIDAuthMode, v as FFIDLogger, w as FFIDCacheAdapter, x as FFIDUser, y as FFIDOrganization, z as FFIDSubscription, A as FFIDSubscriptionContextValue, E as EffectiveSubscriptionStatus, B as FFIDOAuthUserInfoSubscription, C as FFIDAnnouncementsClientConfig, L as ListAnnouncementsOptions, D as FFIDAnnouncementsApiResponse, G as AnnouncementListResponse, H as FFIDAnnouncementsLogger } from './index-_oO5sHjX.cjs';
2
- export { I as Announcement, J as AnnouncementStatus, K as AnnouncementType, M as FFIDAnnouncementBadge, N as FFIDAnnouncementList, O as FFIDAnnouncementsError, P as FFIDAnnouncementsErrorCode, Q as FFIDAnnouncementsServerResponse, R as FFIDCacheConfig, S as FFIDContextValue, T as FFIDInquiryCategory, U as FFIDInquiryCategorySite2026, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a3 as FFIDJwtClaims, a4 as FFIDLoginButton, a5 as FFIDOAuthTokenResponse, a6 as FFIDOAuthUserInfoMemberRole, a7 as FFIDOrganizationSwitcher, a8 as FFIDRedirectErrorCode, a9 as FFIDSeatModel, aa as FFIDServiceAccessDenialReason, ab as FFIDServiceAccessFailPolicy, ac as FFIDSubscriptionBadge, ad as FFIDTokenIntrospectionResponse, ae as FFIDUserMenu, af as FFID_INQUIRY_CATEGORIES, ag as FFID_INQUIRY_CATEGORIES_SITE_2026, ah as UseFFIDAnnouncementsOptions, ai as UseFFIDAnnouncementsReturn, aj as isFFIDInquiryCategorySite2026, ak as useFFIDAnnouncements } from './index-_oO5sHjX.cjs';
1
+ import { F as FFIDSubscriptionStatus, a as FFIDConfig, b as FFIDApiResponse, c as FFIDSessionResponse, d as FFIDRedirectResult, e as FFIDError, f as FFIDSubscriptionCheckResponse, g as FFIDCheckServiceAccessParams, h as FFIDServiceAccessDecision, i as FFIDProfileCallOptions, j as FFIDUserProfile, k as FFIDUpdateUserProfileRequest, l as FFIDAnalyticsConfig, m as FFIDCreateCheckoutParams, n as FFIDCheckoutSessionResponse, o as FFIDCreatePortalParams, p as FFIDPortalSessionResponse, q as FFIDVerifyAccessTokenOptions, r as FFIDOAuthUserInfo, s as FFIDInquiryCreateParams, t as FFIDInquiryCreateResponse, u as FFIDAuthMode, v as FFIDLogger, w as FFIDCacheAdapter, x as FFIDUser, y as FFIDOrganization, z as FFIDSubscription, A as FFIDSubscriptionContextValue, E as EffectiveSubscriptionStatus, B as FFIDOAuthUserInfoSubscription, C as FFIDAnnouncementsClientConfig, L as ListAnnouncementsOptions, D as FFIDAnnouncementsApiResponse, G as AnnouncementListResponse, H as FFIDAnnouncementsLogger } from './index-EFp_ZgK9.cjs';
2
+ export { I as Announcement, J as AnnouncementStatus, K as AnnouncementType, M as FFIDAnnouncementBadge, N as FFIDAnnouncementList, O as FFIDAnnouncementsError, P as FFIDAnnouncementsErrorCode, Q as FFIDAnnouncementsServerResponse, R as FFIDCacheConfig, S as FFIDContextValue, T as FFIDInquiryCategory, U as FFIDInquiryCategorySite2026, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a3 as FFIDJwtClaims, a4 as FFIDLoginButton, a5 as FFIDOAuthTokenResponse, a6 as FFIDOAuthUserInfoMemberRole, a7 as FFIDOrganizationSwitcher, a8 as FFIDRedirectErrorCode, a9 as FFIDSeatModel, aa as FFIDServiceAccessDenialReason, ab as FFIDServiceAccessFailPolicy, ac as FFIDSubscriptionBadge, ad as FFIDTokenIntrospectionResponse, ae as FFIDUserMenu, af as FFID_INQUIRY_CATEGORIES, ag as FFID_INQUIRY_CATEGORIES_SITE_2026, ah as UseFFIDAnnouncementsOptions, ai as UseFFIDAnnouncementsReturn, aj as isFFIDInquiryCategorySite2026, ak as useFFIDAnnouncements } from './index-EFp_ZgK9.cjs';
3
3
  export { A as ALL_DENIED_EXCEPT_NECESSARY, i as CONSENT_COOKIE_NAME, j as CONSENT_DISMISSAL_TIMESTAMP_KEY, l as COOKIE_VERSION, D as DEFAULT_CONSENT_ERROR_MESSAGES, o as FFIDAnalyticsProvider, p as FFIDAnalyticsProviderProps, c as FFIDConsentCategories, r as FFIDConsentCategoryCode, e as FFIDConsentCategoryMetadata, w as FFIDConsentError, x as FFIDConsentErrorCode, y as FFIDConsentMergeStrategy, B as FFIDConsentMergeWarning, E as FFIDConsentMergeWarningEvent, F as FFIDConsentResult, I as FFIDConsentSource, a as FFIDConsentState, d as FFIDConsentSyncResult, M as FFIDCookieBanner, N as FFIDCookieBannerClassNames, O as FFIDCookieBannerProps, Q as FFIDCookieLink, R as FFIDCookieLinkProps, f as FFIDCookiePolicy, T as FFIDCookieSettings, U as FFIDCookieSettingsClassNames, V as FFIDCookieSettingsProps, _ as FFID_CONSENT_ERROR_CODES, ae as UseFFIDConsentPreferencesReturn, af as UseFFIDConsentReturn, ag as clearConsentDismissalTimestamp, aj as decodeConsentCookie, al as encodeConsentCookie, an as readConsentCookie, ao as readConsentDismissalTimestamp, aq as useFFIDConsent, ar as useFFIDConsentPreferences, as as writeConsentCookie, at as writeConsentDismissalTimestamp } from './FFIDCookieLink-CE01gYc4.cjs';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
  import { ReactNode, ComponentType, FC } from 'react';
@@ -1089,6 +1089,20 @@ interface ComputeEffectiveStatusInput {
1089
1089
  * `blocked`.
1090
1090
  */
1091
1091
  pastDueSince?: string | null | undefined;
1092
+ /**
1093
+ * Whether the subscription is scheduled to cancel at the end of the
1094
+ * current billing period (`cancel_at_period_end=true` in Stripe).
1095
+ *
1096
+ * Optional / defaults to `false` for backward compatibility — pre-#3460
1097
+ * callers do not pass this field and MUST keep observing `'active'` for
1098
+ * the same inputs. When `true` and the period has not yet ended, the
1099
+ * `'active'` branch returns `'active_canceling'` so consumers can render
1100
+ * a "cancellation scheduled" badge + reactivation CTA without losing
1101
+ * access. Has no effect on any other DB status branch — runtime
1102
+ * `expired` still wins inside `'active'`, and Stripe does not set
1103
+ * `cancel_at_period_end` on terminal states.
1104
+ */
1105
+ cancelAtPeriodEnd?: boolean | undefined;
1092
1106
  }
1093
1107
  /**
1094
1108
  * Compute an `EffectiveSubscriptionStatus` from the fields available in the
@@ -1131,6 +1145,13 @@ declare function computeEffectiveStatusFromSession(input: ComputeEffectiveStatus
1131
1145
  * caller that needs to fan in on the same access predicate. Mirrors the FFID
1132
1146
  * backend constants in `src/app/api/v1/subscriptions/ext/check/route.ts`
1133
1147
  * (route-local) — change there forces a sync here in the same PR.
1148
+ *
1149
+ * `active_canceling` (post-#3460) is included for forward compatibility:
1150
+ * the FFID backend currently collapses cancellation-scheduled subscriptions
1151
+ * back to `active` on `/ext/check`, but if a future change promotes
1152
+ * `cancel_at_period_end` into the server's `EffectiveSubscriptionStatus`
1153
+ * mapping the userinfo path will already treat it as access-granting
1154
+ * without an additional SDK release.
1134
1155
  */
1135
1156
  declare const ACCESS_GRANTING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[];
1136
1157
  /**
@@ -1145,7 +1166,8 @@ declare const BLOCKING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[
1145
1166
  * Decide whether the `/oauth/userinfo` subscription summary grants access.
1146
1167
  *
1147
1168
  * Returns `true` iff `sub.effectiveStatus` is in
1148
- * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active` or `past_due_grace`).
1169
+ * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active`, `active_canceling`,
1170
+ * or `past_due_grace`).
1149
1171
  *
1150
1172
  * Conservative fallback rules (security-critical — treat as part of the
1151
1173
  * contract, not implementation detail):
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FFIDSubscriptionStatus, a as FFIDConfig, b as FFIDApiResponse, c as FFIDSessionResponse, d as FFIDRedirectResult, e as FFIDError, f as FFIDSubscriptionCheckResponse, g as FFIDCheckServiceAccessParams, h as FFIDServiceAccessDecision, i as FFIDProfileCallOptions, j as FFIDUserProfile, k as FFIDUpdateUserProfileRequest, l as FFIDAnalyticsConfig, m as FFIDCreateCheckoutParams, n as FFIDCheckoutSessionResponse, o as FFIDCreatePortalParams, p as FFIDPortalSessionResponse, q as FFIDVerifyAccessTokenOptions, r as FFIDOAuthUserInfo, s as FFIDInquiryCreateParams, t as FFIDInquiryCreateResponse, u as FFIDAuthMode, v as FFIDLogger, w as FFIDCacheAdapter, x as FFIDUser, y as FFIDOrganization, z as FFIDSubscription, A as FFIDSubscriptionContextValue, E as EffectiveSubscriptionStatus, B as FFIDOAuthUserInfoSubscription, C as FFIDAnnouncementsClientConfig, L as ListAnnouncementsOptions, D as FFIDAnnouncementsApiResponse, G as AnnouncementListResponse, H as FFIDAnnouncementsLogger } from './index-_oO5sHjX.js';
2
- export { I as Announcement, J as AnnouncementStatus, K as AnnouncementType, M as FFIDAnnouncementBadge, N as FFIDAnnouncementList, O as FFIDAnnouncementsError, P as FFIDAnnouncementsErrorCode, Q as FFIDAnnouncementsServerResponse, R as FFIDCacheConfig, S as FFIDContextValue, T as FFIDInquiryCategory, U as FFIDInquiryCategorySite2026, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a3 as FFIDJwtClaims, a4 as FFIDLoginButton, a5 as FFIDOAuthTokenResponse, a6 as FFIDOAuthUserInfoMemberRole, a7 as FFIDOrganizationSwitcher, a8 as FFIDRedirectErrorCode, a9 as FFIDSeatModel, aa as FFIDServiceAccessDenialReason, ab as FFIDServiceAccessFailPolicy, ac as FFIDSubscriptionBadge, ad as FFIDTokenIntrospectionResponse, ae as FFIDUserMenu, af as FFID_INQUIRY_CATEGORIES, ag as FFID_INQUIRY_CATEGORIES_SITE_2026, ah as UseFFIDAnnouncementsOptions, ai as UseFFIDAnnouncementsReturn, aj as isFFIDInquiryCategorySite2026, ak as useFFIDAnnouncements } from './index-_oO5sHjX.js';
1
+ import { F as FFIDSubscriptionStatus, a as FFIDConfig, b as FFIDApiResponse, c as FFIDSessionResponse, d as FFIDRedirectResult, e as FFIDError, f as FFIDSubscriptionCheckResponse, g as FFIDCheckServiceAccessParams, h as FFIDServiceAccessDecision, i as FFIDProfileCallOptions, j as FFIDUserProfile, k as FFIDUpdateUserProfileRequest, l as FFIDAnalyticsConfig, m as FFIDCreateCheckoutParams, n as FFIDCheckoutSessionResponse, o as FFIDCreatePortalParams, p as FFIDPortalSessionResponse, q as FFIDVerifyAccessTokenOptions, r as FFIDOAuthUserInfo, s as FFIDInquiryCreateParams, t as FFIDInquiryCreateResponse, u as FFIDAuthMode, v as FFIDLogger, w as FFIDCacheAdapter, x as FFIDUser, y as FFIDOrganization, z as FFIDSubscription, A as FFIDSubscriptionContextValue, E as EffectiveSubscriptionStatus, B as FFIDOAuthUserInfoSubscription, C as FFIDAnnouncementsClientConfig, L as ListAnnouncementsOptions, D as FFIDAnnouncementsApiResponse, G as AnnouncementListResponse, H as FFIDAnnouncementsLogger } from './index-EFp_ZgK9.js';
2
+ export { I as Announcement, J as AnnouncementStatus, K as AnnouncementType, M as FFIDAnnouncementBadge, N as FFIDAnnouncementList, O as FFIDAnnouncementsError, P as FFIDAnnouncementsErrorCode, Q as FFIDAnnouncementsServerResponse, R as FFIDCacheConfig, S as FFIDContextValue, T as FFIDInquiryCategory, U as FFIDInquiryCategorySite2026, V as FFIDInquiryForm, W as FFIDInquiryFormCategoryItem, X as FFIDInquiryFormClassNames, Y as FFIDInquiryFormLegalLayout, Z as FFIDInquiryFormOrganization, _ as FFIDInquiryFormPlaceholderContext, $ as FFIDInquiryFormPrefill, a0 as FFIDInquiryFormProps, a1 as FFIDInquiryFormSubmitData, a2 as FFIDInquiryFormSubmitResult, a3 as FFIDJwtClaims, a4 as FFIDLoginButton, a5 as FFIDOAuthTokenResponse, a6 as FFIDOAuthUserInfoMemberRole, a7 as FFIDOrganizationSwitcher, a8 as FFIDRedirectErrorCode, a9 as FFIDSeatModel, aa as FFIDServiceAccessDenialReason, ab as FFIDServiceAccessFailPolicy, ac as FFIDSubscriptionBadge, ad as FFIDTokenIntrospectionResponse, ae as FFIDUserMenu, af as FFID_INQUIRY_CATEGORIES, ag as FFID_INQUIRY_CATEGORIES_SITE_2026, ah as UseFFIDAnnouncementsOptions, ai as UseFFIDAnnouncementsReturn, aj as isFFIDInquiryCategorySite2026, ak as useFFIDAnnouncements } from './index-EFp_ZgK9.js';
3
3
  export { A as ALL_DENIED_EXCEPT_NECESSARY, i as CONSENT_COOKIE_NAME, j as CONSENT_DISMISSAL_TIMESTAMP_KEY, l as COOKIE_VERSION, D as DEFAULT_CONSENT_ERROR_MESSAGES, o as FFIDAnalyticsProvider, p as FFIDAnalyticsProviderProps, c as FFIDConsentCategories, r as FFIDConsentCategoryCode, e as FFIDConsentCategoryMetadata, w as FFIDConsentError, x as FFIDConsentErrorCode, y as FFIDConsentMergeStrategy, B as FFIDConsentMergeWarning, E as FFIDConsentMergeWarningEvent, F as FFIDConsentResult, I as FFIDConsentSource, a as FFIDConsentState, d as FFIDConsentSyncResult, M as FFIDCookieBanner, N as FFIDCookieBannerClassNames, O as FFIDCookieBannerProps, Q as FFIDCookieLink, R as FFIDCookieLinkProps, f as FFIDCookiePolicy, T as FFIDCookieSettings, U as FFIDCookieSettingsClassNames, V as FFIDCookieSettingsProps, _ as FFID_CONSENT_ERROR_CODES, ae as UseFFIDConsentPreferencesReturn, af as UseFFIDConsentReturn, ag as clearConsentDismissalTimestamp, aj as decodeConsentCookie, al as encodeConsentCookie, an as readConsentCookie, ao as readConsentDismissalTimestamp, aq as useFFIDConsent, ar as useFFIDConsentPreferences, as as writeConsentCookie, at as writeConsentDismissalTimestamp } from './FFIDCookieLink-CE01gYc4.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
  import { ReactNode, ComponentType, FC } from 'react';
@@ -1089,6 +1089,20 @@ interface ComputeEffectiveStatusInput {
1089
1089
  * `blocked`.
1090
1090
  */
1091
1091
  pastDueSince?: string | null | undefined;
1092
+ /**
1093
+ * Whether the subscription is scheduled to cancel at the end of the
1094
+ * current billing period (`cancel_at_period_end=true` in Stripe).
1095
+ *
1096
+ * Optional / defaults to `false` for backward compatibility — pre-#3460
1097
+ * callers do not pass this field and MUST keep observing `'active'` for
1098
+ * the same inputs. When `true` and the period has not yet ended, the
1099
+ * `'active'` branch returns `'active_canceling'` so consumers can render
1100
+ * a "cancellation scheduled" badge + reactivation CTA without losing
1101
+ * access. Has no effect on any other DB status branch — runtime
1102
+ * `expired` still wins inside `'active'`, and Stripe does not set
1103
+ * `cancel_at_period_end` on terminal states.
1104
+ */
1105
+ cancelAtPeriodEnd?: boolean | undefined;
1092
1106
  }
1093
1107
  /**
1094
1108
  * Compute an `EffectiveSubscriptionStatus` from the fields available in the
@@ -1131,6 +1145,13 @@ declare function computeEffectiveStatusFromSession(input: ComputeEffectiveStatus
1131
1145
  * caller that needs to fan in on the same access predicate. Mirrors the FFID
1132
1146
  * backend constants in `src/app/api/v1/subscriptions/ext/check/route.ts`
1133
1147
  * (route-local) — change there forces a sync here in the same PR.
1148
+ *
1149
+ * `active_canceling` (post-#3460) is included for forward compatibility:
1150
+ * the FFID backend currently collapses cancellation-scheduled subscriptions
1151
+ * back to `active` on `/ext/check`, but if a future change promotes
1152
+ * `cancel_at_period_end` into the server's `EffectiveSubscriptionStatus`
1153
+ * mapping the userinfo path will already treat it as access-granting
1154
+ * without an additional SDK release.
1134
1155
  */
1135
1156
  declare const ACCESS_GRANTING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[];
1136
1157
  /**
@@ -1145,7 +1166,8 @@ declare const BLOCKING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[
1145
1166
  * Decide whether the `/oauth/userinfo` subscription summary grants access.
1146
1167
  *
1147
1168
  * Returns `true` iff `sub.effectiveStatus` is in
1148
- * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active` or `past_due_grace`).
1169
+ * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active`, `active_canceling`,
1170
+ * or `past_due_grace`).
1149
1171
  *
1150
1172
  * Conservative fallback rules (security-critical — treat as part of the
1151
1173
  * contract, not implementation detail):
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useFFIDContext, useSubscription } from './chunk-4SJKXEYB.js';
2
- export { ACCESS_GRANTING_EFFECTIVE_STATUSES, BLOCKING_EFFECTIVE_STATUSES, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, computeEffectiveStatusFromSession, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, hasAccessFromUserinfo, isBlockedFromUserinfo, isFFIDInquiryCategorySite2026, normalizeRedirectUri, retrieveCodeVerifier, storeCodeVerifier, useFFID, useFFIDAnnouncements, useSubscription, withSubscription } from './chunk-4SJKXEYB.js';
1
+ import { useFFIDContext, useSubscription } from './chunk-YAJN36SD.js';
2
+ export { ACCESS_GRANTING_EFFECTIVE_STATUSES, BLOCKING_EFFECTIVE_STATUSES, DEFAULT_API_BASE_URL, DEFAULT_OAUTH_SCOPES, FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDProvider, FFIDSDKError, FFIDSubscriptionBadge, FFIDUserMenu, FFID_ANNOUNCEMENTS_ERROR_CODES, FFID_INQUIRY_CATEGORIES, FFID_INQUIRY_CATEGORIES_SITE_2026, computeEffectiveStatusFromSession, createFFIDAnnouncementsClient, createFFIDClient, createTokenStore, generateCodeChallenge, generateCodeVerifier, hasAccessFromUserinfo, isBlockedFromUserinfo, isFFIDInquiryCategorySite2026, normalizeRedirectUri, retrieveCodeVerifier, storeCodeVerifier, useFFID, useFFIDAnnouncements, useSubscription, withSubscription } from './chunk-YAJN36SD.js';
3
3
  export { ALL_DENIED_EXCEPT_NECESSARY, CONSENT_COOKIE_NAME, CONSENT_DISMISSAL_TIMESTAMP_KEY, COOKIE_VERSION, DEFAULT_CONSENT_ERROR_MESSAGES, FFIDAnalyticsProvider, FFIDConsentError, FFIDCookieBanner, FFIDCookieLink, FFIDCookieSettings, FFID_CONSENT_ERROR_CODES, clearConsentDismissalTimestamp, decodeConsentCookie, encodeConsentCookie, readConsentCookie, readConsentDismissalTimestamp, useFFIDConsent, useFFIDConsentPreferences, writeConsentCookie, writeConsentDismissalTimestamp } from './chunk-KJ7FUNA6.js';
4
4
  import { useEffect, useRef } from 'react';
5
5
  import { jsx, Fragment } from 'react/jsx-runtime';
@@ -837,7 +837,7 @@ function createProfileMethods(deps) {
837
837
  }
838
838
 
839
839
  // src/client/version-check.ts
840
- var SDK_VERSION = "5.6.0";
840
+ var SDK_VERSION = "5.7.0";
841
841
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
842
842
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
843
843
  function sdkHeaders() {
@@ -2190,6 +2190,7 @@ function createInquiryMethods(deps) {
2190
2190
  // src/subscriptions/userinfo-helpers.ts
2191
2191
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2192
2192
  "active",
2193
+ "active_canceling",
2193
2194
  "past_due_grace"
2194
2195
  ];
2195
2196
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2229,6 +2230,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2229
2230
  var DEFAULT_ALLOW_GRACE = true;
2230
2231
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2231
2232
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2233
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2234
+ blocked: "blocked",
2235
+ canceled: "canceled",
2236
+ expired: "expired",
2237
+ trial_expired: "trial_expired"
2238
+ };
2232
2239
  function resolveServiceAccessDenialReason(params, allowGrace) {
2233
2240
  if (params.hasAccess) {
2234
2241
  return null;
@@ -2239,10 +2246,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2239
2246
  if (params.isGrace && !allowGrace) {
2240
2247
  return "grace_disallowed";
2241
2248
  }
2242
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2249
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2243
2250
  return "blocked";
2244
2251
  }
2245
- return params.effectiveStatus;
2252
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2246
2253
  }
2247
2254
  function toServiceAccessDecision(response, allowGrace) {
2248
2255
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -1,8 +1,8 @@
1
- import { F as FFIDLogger, a as FFIDError, b as FFIDCacheAdapter, c as FFIDVerifyAccessTokenOptions, d as FFIDApiResponse, e as FFIDOAuthUserInfo } from '../ffid-client-CmxQxkoK.cjs';
2
- export { f as FFIDAddMemberParams, g as FFIDAddMemberRequest, h as FFIDAddMemberResponse, i as FFIDAssignableMemberRole, j as FFIDCacheConfig, k as FFIDClient, l as FFIDConfig, m as FFIDListMembersResponse, n as FFIDMemberRole, o as FFIDOrganization, p as FFIDOrganizationMember, q as FFIDOtpSendResponse, r as FFIDOtpVerifyResponse, s as FFIDPasswordResetConfirmResponse, t as FFIDPasswordResetResponse, u as FFIDPasswordResetVerifyResponse, v as FFIDProfileCallOptions, w as FFIDRemoveMemberResponse, x as FFIDResetSessionResponse, y as FFIDSubscription, z as FFIDUpdateMemberRoleResponse, A as FFIDUpdateUserProfileRequest, B as FFIDUser, C as FFIDUserProfile, T as TokenData, D as TokenStore, E as createFFIDClient, G as createTokenStore } from '../ffid-client-CmxQxkoK.cjs';
1
+ import { F as FFIDLogger, a as FFIDError, b as FFIDCacheAdapter, c as FFIDVerifyAccessTokenOptions, d as FFIDApiResponse, e as FFIDOAuthUserInfo } from '../ffid-client-CknwTgly.cjs';
2
+ export { f as FFIDAddMemberParams, g as FFIDAddMemberRequest, h as FFIDAddMemberResponse, i as FFIDAssignableMemberRole, j as FFIDCacheConfig, k as FFIDClient, l as FFIDConfig, m as FFIDListMembersResponse, n as FFIDMemberRole, o as FFIDOrganization, p as FFIDOrganizationMember, q as FFIDOtpSendResponse, r as FFIDOtpVerifyResponse, s as FFIDPasswordResetConfirmResponse, t as FFIDPasswordResetResponse, u as FFIDPasswordResetVerifyResponse, v as FFIDProfileCallOptions, w as FFIDRemoveMemberResponse, x as FFIDResetSessionResponse, y as FFIDSubscription, z as FFIDUpdateMemberRoleResponse, A as FFIDUpdateUserProfileRequest, B as FFIDUser, C as FFIDUserProfile, T as TokenData, D as TokenStore, E as createFFIDClient, G as createTokenStore } from '../ffid-client-CknwTgly.cjs';
3
3
  export { D as DEFAULT_API_BASE_URL, a as DEFAULT_OAUTH_SCOPES } from '../constants-D61jqRIO.cjs';
4
4
  import { z } from 'zod';
5
- import '../types-5g_Bg6Ey.cjs';
5
+ import '../types-s3y11uT6.cjs';
6
6
 
7
7
  /** Token verification - verifyAccessToken() implementation */
8
8
 
@@ -1,8 +1,8 @@
1
- import { F as FFIDLogger, a as FFIDError, b as FFIDCacheAdapter, c as FFIDVerifyAccessTokenOptions, d as FFIDApiResponse, e as FFIDOAuthUserInfo } from '../ffid-client-BXl7Dop7.js';
2
- export { f as FFIDAddMemberParams, g as FFIDAddMemberRequest, h as FFIDAddMemberResponse, i as FFIDAssignableMemberRole, j as FFIDCacheConfig, k as FFIDClient, l as FFIDConfig, m as FFIDListMembersResponse, n as FFIDMemberRole, o as FFIDOrganization, p as FFIDOrganizationMember, q as FFIDOtpSendResponse, r as FFIDOtpVerifyResponse, s as FFIDPasswordResetConfirmResponse, t as FFIDPasswordResetResponse, u as FFIDPasswordResetVerifyResponse, v as FFIDProfileCallOptions, w as FFIDRemoveMemberResponse, x as FFIDResetSessionResponse, y as FFIDSubscription, z as FFIDUpdateMemberRoleResponse, A as FFIDUpdateUserProfileRequest, B as FFIDUser, C as FFIDUserProfile, T as TokenData, D as TokenStore, E as createFFIDClient, G as createTokenStore } from '../ffid-client-BXl7Dop7.js';
1
+ import { F as FFIDLogger, a as FFIDError, b as FFIDCacheAdapter, c as FFIDVerifyAccessTokenOptions, d as FFIDApiResponse, e as FFIDOAuthUserInfo } from '../ffid-client-C0k2PTbO.js';
2
+ export { f as FFIDAddMemberParams, g as FFIDAddMemberRequest, h as FFIDAddMemberResponse, i as FFIDAssignableMemberRole, j as FFIDCacheConfig, k as FFIDClient, l as FFIDConfig, m as FFIDListMembersResponse, n as FFIDMemberRole, o as FFIDOrganization, p as FFIDOrganizationMember, q as FFIDOtpSendResponse, r as FFIDOtpVerifyResponse, s as FFIDPasswordResetConfirmResponse, t as FFIDPasswordResetResponse, u as FFIDPasswordResetVerifyResponse, v as FFIDProfileCallOptions, w as FFIDRemoveMemberResponse, x as FFIDResetSessionResponse, y as FFIDSubscription, z as FFIDUpdateMemberRoleResponse, A as FFIDUpdateUserProfileRequest, B as FFIDUser, C as FFIDUserProfile, T as TokenData, D as TokenStore, E as createFFIDClient, G as createTokenStore } from '../ffid-client-C0k2PTbO.js';
3
3
  export { D as DEFAULT_API_BASE_URL, a as DEFAULT_OAUTH_SCOPES } from '../constants-D61jqRIO.js';
4
4
  import { z } from 'zod';
5
- import '../types-5g_Bg6Ey.js';
5
+ import '../types-s3y11uT6.js';
6
6
 
7
7
  /** Token verification - verifyAccessToken() implementation */
8
8
 
@@ -836,7 +836,7 @@ function createProfileMethods(deps) {
836
836
  }
837
837
 
838
838
  // src/client/version-check.ts
839
- var SDK_VERSION = "5.6.0";
839
+ var SDK_VERSION = "5.7.0";
840
840
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
841
841
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
842
842
  function sdkHeaders() {
@@ -2189,6 +2189,7 @@ function createInquiryMethods(deps) {
2189
2189
  // src/subscriptions/userinfo-helpers.ts
2190
2190
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2191
2191
  "active",
2192
+ "active_canceling",
2192
2193
  "past_due_grace"
2193
2194
  ];
2194
2195
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2228,6 +2229,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2228
2229
  var DEFAULT_ALLOW_GRACE = true;
2229
2230
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2230
2231
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2232
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2233
+ blocked: "blocked",
2234
+ canceled: "canceled",
2235
+ expired: "expired",
2236
+ trial_expired: "trial_expired"
2237
+ };
2231
2238
  function resolveServiceAccessDenialReason(params, allowGrace) {
2232
2239
  if (params.hasAccess) {
2233
2240
  return null;
@@ -2238,10 +2245,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2238
2245
  if (params.isGrace && !allowGrace) {
2239
2246
  return "grace_disallowed";
2240
2247
  }
2241
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2248
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2242
2249
  return "blocked";
2243
2250
  }
2244
- return params.effectiveStatus;
2251
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2245
2252
  }
2246
2253
  function toServiceAccessDecision(response, allowGrace) {
2247
2254
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -1,5 +1,5 @@
1
- import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-CmxQxkoK.cjs';
2
- import '../../types-5g_Bg6Ey.cjs';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-CknwTgly.cjs';
2
+ import '../../types-s3y11uT6.cjs';
3
3
 
4
4
  /**
5
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -1,5 +1,5 @@
1
- import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-BXl7Dop7.js';
2
- import '../../types-5g_Bg6Ey.js';
1
+ import { k as FFIDClient, e as FFIDOAuthUserInfo } from '../../ffid-client-C0k2PTbO.js';
2
+ import '../../types-s3y11uT6.js';
3
3
 
4
4
  /**
5
5
  * FFID SDK - Test mode client (E2E / integration test bypass)
@@ -17,6 +17,10 @@
17
17
  * branch on a single value:
18
18
  *
19
19
  * - `active` — normal paying subscription; grant full access.
20
+ * - `active_canceling` — paying subscription with `cancel_at_period_end=true`.
21
+ * Access stays live until `currentPeriodEnd`; callers SHOULD surface a
22
+ * "cancellation scheduled" badge and a reactivation CTA. Session-only
23
+ * refinement of `active` — see the asymmetry note below.
20
24
  * - `past_due_grace` — payment failed but FFID is still retrying. Service
21
25
  * SHOULD keep access and surface a recovery banner.
22
26
  * - `blocked` — payment dunning exhausted (`past_due` over grace,
@@ -29,9 +33,26 @@
29
33
  * This is the FFID-internal runtime-expired case (see
30
34
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
35
  *
36
+ * **SDK / server asymmetry — `active_canceling` is session-only (#3460).**
37
+ * The backend `EffectiveSubscriptionStatus` in
38
+ * `src/lib/common/subscription-helpers.ts` does NOT currently include
39
+ * `active_canceling`; cancellation-scheduled subscriptions surface as plain
40
+ * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
41
+ * consumers that branch on the server verdict will not see the new literal.
42
+ * The SDK enriches `computeEffectiveStatusFromSession` with
43
+ * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
44
+ * #3460 onward — older backends omit the field and the helper defaults to
45
+ * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
46
+ * can surface a "cancellation scheduled" badge without waiting for an
47
+ * `/ext/check` round-trip. The asymmetry is safe because the new literal is
48
+ * access-granting (no security regression on the userinfo path); when the
49
+ * server's `EffectiveSubscriptionStatus` is later extended to emit
50
+ * `active_canceling`, this docstring MUST be updated to drop the
51
+ * session-only note.
52
+ *
32
53
  * @see /api/v1/subscriptions/ext/check
33
54
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
55
  */
35
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
56
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
57
 
37
58
  export type { EffectiveSubscriptionStatus as E };
@@ -17,6 +17,10 @@
17
17
  * branch on a single value:
18
18
  *
19
19
  * - `active` — normal paying subscription; grant full access.
20
+ * - `active_canceling` — paying subscription with `cancel_at_period_end=true`.
21
+ * Access stays live until `currentPeriodEnd`; callers SHOULD surface a
22
+ * "cancellation scheduled" badge and a reactivation CTA. Session-only
23
+ * refinement of `active` — see the asymmetry note below.
20
24
  * - `past_due_grace` — payment failed but FFID is still retrying. Service
21
25
  * SHOULD keep access and surface a recovery banner.
22
26
  * - `blocked` — payment dunning exhausted (`past_due` over grace,
@@ -29,9 +33,26 @@
29
33
  * This is the FFID-internal runtime-expired case (see
30
34
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
35
  *
36
+ * **SDK / server asymmetry — `active_canceling` is session-only (#3460).**
37
+ * The backend `EffectiveSubscriptionStatus` in
38
+ * `src/lib/common/subscription-helpers.ts` does NOT currently include
39
+ * `active_canceling`; cancellation-scheduled subscriptions surface as plain
40
+ * `'active'` on `/api/v1/subscriptions/ext/check` and `/oauth/userinfo`, and
41
+ * consumers that branch on the server verdict will not see the new literal.
42
+ * The SDK enriches `computeEffectiveStatusFromSession` with
43
+ * `cancelAtPeriodEnd` (carried on `FFIDSubscription` for FFID backends from
44
+ * #3460 onward — older backends omit the field and the helper defaults to
45
+ * `false`, preserving the pre-#3460 `'active'` verdict) so `useSubscription`
46
+ * can surface a "cancellation scheduled" badge without waiting for an
47
+ * `/ext/check` round-trip. The asymmetry is safe because the new literal is
48
+ * access-granting (no security regression on the userinfo path); when the
49
+ * server's `EffectiveSubscriptionStatus` is later extended to emit
50
+ * `active_canceling`, this docstring MUST be updated to drop the
51
+ * session-only note.
52
+ *
32
53
  * @see /api/v1/subscriptions/ext/check
33
54
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
55
  */
35
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
56
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
57
 
37
58
  export type { EffectiveSubscriptionStatus as E };
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from '../types-5g_Bg6Ey.cjs';
1
+ import { E as EffectiveSubscriptionStatus } from '../types-s3y11uT6.cjs';
2
2
 
3
3
  /**
4
4
  * FFID Webhook SDK Constants
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from '../types-5g_Bg6Ey.js';
1
+ import { E as EffectiveSubscriptionStatus } from '../types-s3y11uT6.js';
2
2
 
3
3
  /**
4
4
  * FFID Webhook SDK Constants
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",