@feelflow/ffid-sdk 5.6.0 → 5.8.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.
@@ -130,7 +130,15 @@ function normalizeUserinfo(raw) {
130
130
  seatModel: raw.subscription.seat_model ?? null,
131
131
  memberRole: raw.subscription.member_role ?? null,
132
132
  organizationId: raw.subscription.organization_id ?? null,
133
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
133
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
134
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
135
+ // The "unknown" axis is unused — no consumer differentiates "server
136
+ // didn't tell us" from "definitely not scheduled" because the
137
+ // companion `effectiveStatus` already returns `'active'` (not
138
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
139
+ // here keeps the downstream contract a plain boolean.
140
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
141
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
134
142
  } : void 0
135
143
  };
136
144
  }
@@ -147,8 +155,14 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
147
155
  planCode: subscription.planCode,
148
156
  planName: subscription.planCode,
149
157
  status: subscription.status,
150
- currentPeriodEnd: null,
158
+ // #3464: userinfo now carries `current_period_end` and
159
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
160
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
161
+ // after `normalizeUserinfo` defaults), so the session helper sees
162
+ // plain `'active'` — identical to pre-#3464 behaviour.
163
+ currentPeriodEnd: subscription.currentPeriodEnd,
151
164
  trialEnd: null,
165
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
152
166
  seatModel: subscription.seatModel ?? void 0,
153
167
  memberRole: subscription.memberRole ?? void 0,
154
168
  organizationId: subscription.organizationId
@@ -842,7 +856,7 @@ function createProfileMethods(deps) {
842
856
  }
843
857
 
844
858
  // src/client/version-check.ts
845
- var SDK_VERSION = "5.6.0";
859
+ var SDK_VERSION = "5.8.0";
846
860
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
847
861
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
848
862
  function sdkHeaders() {
@@ -2247,7 +2261,7 @@ function isExpired(isoTimestamp, nowMs) {
2247
2261
  return parsed < nowMs;
2248
2262
  }
2249
2263
  function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2250
- const { status, currentPeriodEnd, trialEnd, pastDueSince } = input;
2264
+ const { status, currentPeriodEnd, trialEnd, pastDueSince, cancelAtPeriodEnd = false } = input;
2251
2265
  switch (status) {
2252
2266
  case "trialing": {
2253
2267
  const relevantEnd = trialEnd ?? currentPeriodEnd;
@@ -2256,7 +2270,8 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2256
2270
  }
2257
2271
  case "active": {
2258
2272
  const expired = isExpired(currentPeriodEnd, nowMs);
2259
- return expired === true ? "expired" : "active";
2273
+ if (expired === true) return "expired";
2274
+ return cancelAtPeriodEnd ? "active_canceling" : "active";
2260
2275
  }
2261
2276
  case "past_due": {
2262
2277
  const baselineIso = pastDueSince ?? currentPeriodEnd;
@@ -2285,6 +2300,7 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2285
2300
  // src/subscriptions/userinfo-helpers.ts
2286
2301
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2287
2302
  "active",
2303
+ "active_canceling",
2288
2304
  "past_due_grace"
2289
2305
  ];
2290
2306
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2338,6 +2354,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2338
2354
  var DEFAULT_ALLOW_GRACE = true;
2339
2355
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2340
2356
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2357
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2358
+ blocked: "blocked",
2359
+ canceled: "canceled",
2360
+ expired: "expired",
2361
+ trial_expired: "trial_expired"
2362
+ };
2341
2363
  function resolveServiceAccessDenialReason(params, allowGrace) {
2342
2364
  if (params.hasAccess) {
2343
2365
  return null;
@@ -2348,10 +2370,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2348
2370
  if (params.isGrace && !allowGrace) {
2349
2371
  return "grace_disallowed";
2350
2372
  }
2351
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2373
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2352
2374
  return "blocked";
2353
2375
  }
2354
- return params.effectiveStatus;
2376
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2355
2377
  }
2356
2378
  function toServiceAccessDecision(response, allowGrace) {
2357
2379
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -3522,6 +3544,7 @@ function FFIDOrganizationSwitcher({
3522
3544
  }
3523
3545
  var ACCESS_GRANTING_STATUSES = [
3524
3546
  "active",
3547
+ "active_canceling",
3525
3548
  "past_due_grace"
3526
3549
  ];
3527
3550
  var BLOCKING_STATUSES = [
@@ -3558,7 +3581,8 @@ function useSubscription() {
3558
3581
  // のため `computeEffectiveStatusFromSession` 内で currentPeriodEnd を
3559
3582
  // fallback に使う (Stripe は period end 近辺で past_due にする)。
3560
3583
  // Canonical な verdict が必要な consumer は `/ext/check` を直接呼ぶ。
3561
- pastDueSince: void 0
3584
+ pastDueSince: void 0,
3585
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd
3562
3586
  });
3563
3587
  }, [subscription]);
3564
3588
  const isGrace = effectiveStatus === "past_due_grace";
@@ -128,7 +128,15 @@ function normalizeUserinfo(raw) {
128
128
  seatModel: raw.subscription.seat_model ?? null,
129
129
  memberRole: raw.subscription.member_role ?? null,
130
130
  organizationId: raw.subscription.organization_id ?? null,
131
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
131
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
132
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
133
+ // The "unknown" axis is unused — no consumer differentiates "server
134
+ // didn't tell us" from "definitely not scheduled" because the
135
+ // companion `effectiveStatus` already returns `'active'` (not
136
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
137
+ // here keeps the downstream contract a plain boolean.
138
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
139
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
132
140
  } : void 0
133
141
  };
134
142
  }
@@ -145,8 +153,14 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
145
153
  planCode: subscription.planCode,
146
154
  planName: subscription.planCode,
147
155
  status: subscription.status,
148
- currentPeriodEnd: null,
156
+ // #3464: userinfo now carries `current_period_end` and
157
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
158
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
159
+ // after `normalizeUserinfo` defaults), so the session helper sees
160
+ // plain `'active'` — identical to pre-#3464 behaviour.
161
+ currentPeriodEnd: subscription.currentPeriodEnd,
149
162
  trialEnd: null,
163
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
150
164
  seatModel: subscription.seatModel ?? void 0,
151
165
  memberRole: subscription.memberRole ?? void 0,
152
166
  organizationId: subscription.organizationId
@@ -840,7 +854,7 @@ function createProfileMethods(deps) {
840
854
  }
841
855
 
842
856
  // src/client/version-check.ts
843
- var SDK_VERSION = "5.6.0";
857
+ var SDK_VERSION = "5.8.0";
844
858
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
845
859
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
846
860
  function sdkHeaders() {
@@ -2245,7 +2259,7 @@ function isExpired(isoTimestamp, nowMs) {
2245
2259
  return parsed < nowMs;
2246
2260
  }
2247
2261
  function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2248
- const { status, currentPeriodEnd, trialEnd, pastDueSince } = input;
2262
+ const { status, currentPeriodEnd, trialEnd, pastDueSince, cancelAtPeriodEnd = false } = input;
2249
2263
  switch (status) {
2250
2264
  case "trialing": {
2251
2265
  const relevantEnd = trialEnd ?? currentPeriodEnd;
@@ -2254,7 +2268,8 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2254
2268
  }
2255
2269
  case "active": {
2256
2270
  const expired = isExpired(currentPeriodEnd, nowMs);
2257
- return expired === true ? "expired" : "active";
2271
+ if (expired === true) return "expired";
2272
+ return cancelAtPeriodEnd ? "active_canceling" : "active";
2258
2273
  }
2259
2274
  case "past_due": {
2260
2275
  const baselineIso = pastDueSince ?? currentPeriodEnd;
@@ -2283,6 +2298,7 @@ function computeEffectiveStatusFromSession(input, nowMs = Date.now()) {
2283
2298
  // src/subscriptions/userinfo-helpers.ts
2284
2299
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2285
2300
  "active",
2301
+ "active_canceling",
2286
2302
  "past_due_grace"
2287
2303
  ];
2288
2304
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2336,6 +2352,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2336
2352
  var DEFAULT_ALLOW_GRACE = true;
2337
2353
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2338
2354
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2355
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2356
+ blocked: "blocked",
2357
+ canceled: "canceled",
2358
+ expired: "expired",
2359
+ trial_expired: "trial_expired"
2360
+ };
2339
2361
  function resolveServiceAccessDenialReason(params, allowGrace) {
2340
2362
  if (params.hasAccess) {
2341
2363
  return null;
@@ -2346,10 +2368,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2346
2368
  if (params.isGrace && !allowGrace) {
2347
2369
  return "grace_disallowed";
2348
2370
  }
2349
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2371
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2350
2372
  return "blocked";
2351
2373
  }
2352
- return params.effectiveStatus;
2374
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2353
2375
  }
2354
2376
  function toServiceAccessDecision(response, allowGrace) {
2355
2377
  const effectiveStatus = response.effectiveStatus ?? null;
@@ -3520,6 +3542,7 @@ function FFIDOrganizationSwitcher({
3520
3542
  }
3521
3543
  var ACCESS_GRANTING_STATUSES = [
3522
3544
  "active",
3545
+ "active_canceling",
3523
3546
  "past_due_grace"
3524
3547
  ];
3525
3548
  var BLOCKING_STATUSES = [
@@ -3556,7 +3579,8 @@ function useSubscription() {
3556
3579
  // のため `computeEffectiveStatusFromSession` 内で currentPeriodEnd を
3557
3580
  // fallback に使う (Stripe は period end 近辺で past_due にする)。
3558
3581
  // Canonical な verdict が必要な consumer は `/ext/check` を直接呼ぶ。
3559
- pastDueSince: void 0
3582
+ pastDueSince: void 0,
3583
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd
3560
3584
  });
3561
3585
  }, [subscription]);
3562
3586
  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 chunkMRHDPB7H_cjs = require('../chunk-MRHDPB7H.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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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-DeegKrLW.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-DeegKrLW.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-Z6COUAR6.js';
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-5g_Bg6Ey.cjs';
1
+ import { E as EffectiveSubscriptionStatus } from './types-BYdtyO_2.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 */
@@ -748,6 +758,25 @@ interface FFIDOAuthUserInfoSubscription {
748
758
  memberRole: FFIDOAuthUserInfoMemberRole | null;
749
759
  organizationId: string | null;
750
760
  hasSeatAssignment: boolean | null;
761
+ /**
762
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
763
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
764
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
765
+ * the server omits it (older FFID pre-#3464), so consumers always see
766
+ * a plain boolean and never branch on an "unknown" sentinel — the
767
+ * companion `effectiveStatus` already returns `'active'` (not
768
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
769
+ * is implicitly false there.
770
+ */
771
+ cancelAtPeriodEnd: boolean;
772
+ /**
773
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
774
+ * `null` for free / seatless plans, or when serving an older FFID
775
+ * (pre-#3464) that does not populate the field. Exposed so callers
776
+ * can display the cancellation effective date without an `/ext/check`
777
+ * round-trip.
778
+ */
779
+ currentPeriodEnd: string | null;
751
780
  }
752
781
  /**
753
782
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
@@ -1,4 +1,4 @@
1
- import { E as EffectiveSubscriptionStatus } from './types-5g_Bg6Ey.js';
1
+ import { E as EffectiveSubscriptionStatus } from './types-BYdtyO_2.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 */
@@ -748,6 +758,25 @@ interface FFIDOAuthUserInfoSubscription {
748
758
  memberRole: FFIDOAuthUserInfoMemberRole | null;
749
759
  organizationId: string | null;
750
760
  hasSeatAssignment: boolean | null;
761
+ /**
762
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
763
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
764
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
765
+ * the server omits it (older FFID pre-#3464), so consumers always see
766
+ * a plain boolean and never branch on an "unknown" sentinel — the
767
+ * companion `effectiveStatus` already returns `'active'` (not
768
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
769
+ * is implicitly false there.
770
+ */
771
+ cancelAtPeriodEnd: boolean;
772
+ /**
773
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
774
+ * `null` for free / seatless plans, or when serving an older FFID
775
+ * (pre-#3464) that does not populate the field. Exposed so callers
776
+ * can display the cancellation effective date without an `/ext/check`
777
+ * round-trip.
778
+ */
779
+ currentPeriodEnd: string | null;
751
780
  }
752
781
  /**
753
782
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
@@ -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,23 @@ interface FFIDCacheConfig {
53
57
  * This is the FFID-internal runtime-expired case (see
54
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
55
59
  *
60
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
61
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
62
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
63
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
64
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
65
+ * period is still live. SDK consumers receive the same literal whether
66
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
67
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
68
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
69
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
70
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
71
+ * original `'active'` verdict for legacy deployments.
72
+ *
56
73
  * @see /api/v1/subscriptions/ext/check
57
74
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
58
75
  */
59
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
76
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
60
77
 
61
78
  /**
62
79
  * Canonical service-access types for subscription lifecycle decisions.
@@ -230,6 +247,16 @@ interface FFIDSubscription {
230
247
  currentPeriodEnd: string | null;
231
248
  /** Trial end date (null if not a trial or no trial_end set) */
232
249
  trialEnd: string | null;
250
+ /**
251
+ * Whether the subscription is scheduled to cancel at the end of the current
252
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
253
+ * compatibility with FFID backends that pre-date #3460 — when absent,
254
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
255
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
256
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
257
+ * + reactivation CTA without losing access.
258
+ */
259
+ cancelAtPeriodEnd?: boolean;
233
260
  /** Service seat model (available when sourced from OAuth userinfo) */
234
261
  seatModel?: FFIDSeatModel | undefined;
235
262
  /** Member role in the resolved organization context */
@@ -254,6 +281,25 @@ interface FFIDOAuthUserInfoSubscription {
254
281
  memberRole: FFIDOAuthUserInfoMemberRole | null;
255
282
  organizationId: string | null;
256
283
  hasSeatAssignment: boolean | null;
284
+ /**
285
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
286
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
287
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
288
+ * the server omits it (older FFID pre-#3464), so consumers always see
289
+ * a plain boolean and never branch on an "unknown" sentinel — the
290
+ * companion `effectiveStatus` already returns `'active'` (not
291
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
292
+ * is implicitly false there.
293
+ */
294
+ cancelAtPeriodEnd: boolean;
295
+ /**
296
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
297
+ * `null` for free / seatless plans, or when serving an older FFID
298
+ * (pre-#3464) that does not populate the field. Exposed so callers
299
+ * can display the cancellation effective date without an `/ext/check`
300
+ * round-trip.
301
+ */
302
+ currentPeriodEnd: string | null;
257
303
  }
258
304
  /**
259
305
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
@@ -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,23 @@ interface FFIDCacheConfig {
53
57
  * This is the FFID-internal runtime-expired case (see
54
58
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
55
59
  *
60
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
61
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
62
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
63
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
64
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
65
+ * period is still live. SDK consumers receive the same literal whether
66
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
67
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
68
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
69
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
70
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
71
+ * original `'active'` verdict for legacy deployments.
72
+ *
56
73
  * @see /api/v1/subscriptions/ext/check
57
74
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
58
75
  */
59
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
76
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
60
77
 
61
78
  /**
62
79
  * Canonical service-access types for subscription lifecycle decisions.
@@ -230,6 +247,16 @@ interface FFIDSubscription {
230
247
  currentPeriodEnd: string | null;
231
248
  /** Trial end date (null if not a trial or no trial_end set) */
232
249
  trialEnd: string | null;
250
+ /**
251
+ * Whether the subscription is scheduled to cancel at the end of the current
252
+ * billing period (Stripe `cancel_at_period_end`). Optional for backward
253
+ * compatibility with FFID backends that pre-date #3460 — when absent,
254
+ * `useSubscription` treats the value as `false` and surfaces plain `active`.
255
+ * When `true` and the period has not yet ended, `effectiveStatus` becomes
256
+ * `active_canceling` so consumers can render a cancellation-scheduled badge
257
+ * + reactivation CTA without losing access.
258
+ */
259
+ cancelAtPeriodEnd?: boolean;
233
260
  /** Service seat model (available when sourced from OAuth userinfo) */
234
261
  seatModel?: FFIDSeatModel | undefined;
235
262
  /** Member role in the resolved organization context */
@@ -254,6 +281,25 @@ interface FFIDOAuthUserInfoSubscription {
254
281
  memberRole: FFIDOAuthUserInfoMemberRole | null;
255
282
  organizationId: string | null;
256
283
  hasSeatAssignment: boolean | null;
284
+ /**
285
+ * Stripe `cancel_at_period_end` (#3464, SDK 5.8.0). `true` while the
286
+ * subscription is scheduled to terminate at `currentPeriodEnd`. The
287
+ * `normalizeUserinfo` boundary defaults the wire field to `false` when
288
+ * the server omits it (older FFID pre-#3464), so consumers always see
289
+ * a plain boolean and never branch on an "unknown" sentinel — the
290
+ * companion `effectiveStatus` already returns `'active'` (not
291
+ * `'active_canceling'`) for legacy backends since `cancel_at_period_end`
292
+ * is implicitly false there.
293
+ */
294
+ cancelAtPeriodEnd: boolean;
295
+ /**
296
+ * ISO timestamp of the current billing period end (#3464, SDK 5.8.0).
297
+ * `null` for free / seatless plans, or when serving an older FFID
298
+ * (pre-#3464) that does not populate the field. Exposed so callers
299
+ * can display the cancellation effective date without an `/ext/check`
300
+ * round-trip.
301
+ */
302
+ currentPeriodEnd: string | null;
257
303
  }
258
304
  /**
259
305
  * Unified user-identity shape returned by `getSession()` (→ `/oauth/userinfo`)
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 chunkMRHDPB7H_cjs = require('./chunk-MRHDPB7H.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 } = chunkMRHDPB7H_cjs.useFFIDContext();
58
+ const { effectiveStatus, isBlocked, isGrace } = chunkMRHDPB7H_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 } = chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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 chunkMRHDPB7H_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-DeegKrLW.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-DeegKrLW.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,21 @@ 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 (and pre-#3464 FFID backends that omit the field on the wire)
1098
+ * do not pass this field and MUST keep observing `'active'` for the
1099
+ * same inputs. When `true` and the period has not yet ended, the
1100
+ * `'active'` branch returns `'active_canceling'` so consumers can render
1101
+ * a "cancellation scheduled" badge + reactivation CTA without losing
1102
+ * access. Outside `case 'active'` the field is intentionally ignored,
1103
+ * so any non-`active` DB status is unaffected regardless of what Stripe
1104
+ * sets.
1105
+ */
1106
+ cancelAtPeriodEnd?: boolean | undefined;
1092
1107
  }
1093
1108
  /**
1094
1109
  * Compute an `EffectiveSubscriptionStatus` from the fields available in the
@@ -1131,6 +1146,14 @@ declare function computeEffectiveStatusFromSession(input: ComputeEffectiveStatus
1131
1146
  * caller that needs to fan in on the same access predicate. Mirrors the FFID
1132
1147
  * backend constants in `src/app/api/v1/subscriptions/ext/check/route.ts`
1133
1148
  * (route-local) — change there forces a sync here in the same PR.
1149
+ *
1150
+ * `active_canceling` (post-#3464, server-mirrored since SDK 5.8.0) is
1151
+ * access-granting: the FFID backend now emits the literal on
1152
+ * `/oauth/userinfo` and `/ext/check` when `cancel_at_period_end=true` and
1153
+ * the period is still live. `hasAccessFromUserinfo` treats it identically
1154
+ * to `active`; `isBlockedFromUserinfo` returns `false` for it. Callers
1155
+ * that want to render a "cancellation scheduled" badge should branch on
1156
+ * the literal directly.
1134
1157
  */
1135
1158
  declare const ACCESS_GRANTING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[];
1136
1159
  /**
@@ -1145,7 +1168,8 @@ declare const BLOCKING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[
1145
1168
  * Decide whether the `/oauth/userinfo` subscription summary grants access.
1146
1169
  *
1147
1170
  * Returns `true` iff `sub.effectiveStatus` is in
1148
- * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active` or `past_due_grace`).
1171
+ * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active`, `active_canceling`,
1172
+ * or `past_due_grace`).
1149
1173
  *
1150
1174
  * Conservative fallback rules (security-critical — treat as part of the
1151
1175
  * 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-DeegKrLW.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-DeegKrLW.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,21 @@ 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 (and pre-#3464 FFID backends that omit the field on the wire)
1098
+ * do not pass this field and MUST keep observing `'active'` for the
1099
+ * same inputs. When `true` and the period has not yet ended, the
1100
+ * `'active'` branch returns `'active_canceling'` so consumers can render
1101
+ * a "cancellation scheduled" badge + reactivation CTA without losing
1102
+ * access. Outside `case 'active'` the field is intentionally ignored,
1103
+ * so any non-`active` DB status is unaffected regardless of what Stripe
1104
+ * sets.
1105
+ */
1106
+ cancelAtPeriodEnd?: boolean | undefined;
1092
1107
  }
1093
1108
  /**
1094
1109
  * Compute an `EffectiveSubscriptionStatus` from the fields available in the
@@ -1131,6 +1146,14 @@ declare function computeEffectiveStatusFromSession(input: ComputeEffectiveStatus
1131
1146
  * caller that needs to fan in on the same access predicate. Mirrors the FFID
1132
1147
  * backend constants in `src/app/api/v1/subscriptions/ext/check/route.ts`
1133
1148
  * (route-local) — change there forces a sync here in the same PR.
1149
+ *
1150
+ * `active_canceling` (post-#3464, server-mirrored since SDK 5.8.0) is
1151
+ * access-granting: the FFID backend now emits the literal on
1152
+ * `/oauth/userinfo` and `/ext/check` when `cancel_at_period_end=true` and
1153
+ * the period is still live. `hasAccessFromUserinfo` treats it identically
1154
+ * to `active`; `isBlockedFromUserinfo` returns `false` for it. Callers
1155
+ * that want to render a "cancellation scheduled" badge should branch on
1156
+ * the literal directly.
1134
1157
  */
1135
1158
  declare const ACCESS_GRANTING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[];
1136
1159
  /**
@@ -1145,7 +1168,8 @@ declare const BLOCKING_EFFECTIVE_STATUSES: readonly EffectiveSubscriptionStatus[
1145
1168
  * Decide whether the `/oauth/userinfo` subscription summary grants access.
1146
1169
  *
1147
1170
  * Returns `true` iff `sub.effectiveStatus` is in
1148
- * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active` or `past_due_grace`).
1171
+ * {@link ACCESS_GRANTING_EFFECTIVE_STATUSES} (`active`, `active_canceling`,
1172
+ * or `past_due_grace`).
1149
1173
  *
1150
1174
  * Conservative fallback rules (security-critical — treat as part of the
1151
1175
  * 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-Z6COUAR6.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-Z6COUAR6.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';
@@ -125,7 +125,15 @@ function normalizeUserinfo(raw) {
125
125
  seatModel: raw.subscription.seat_model ?? null,
126
126
  memberRole: raw.subscription.member_role ?? null,
127
127
  organizationId: raw.subscription.organization_id ?? null,
128
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
128
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
129
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
130
+ // The "unknown" axis is unused — no consumer differentiates "server
131
+ // didn't tell us" from "definitely not scheduled" because the
132
+ // companion `effectiveStatus` already returns `'active'` (not
133
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
134
+ // here keeps the downstream contract a plain boolean.
135
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
136
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
129
137
  } : void 0
130
138
  };
131
139
  }
@@ -142,8 +150,14 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
142
150
  planCode: subscription.planCode,
143
151
  planName: subscription.planCode,
144
152
  status: subscription.status,
145
- currentPeriodEnd: null,
153
+ // #3464: userinfo now carries `current_period_end` and
154
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
155
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
156
+ // after `normalizeUserinfo` defaults), so the session helper sees
157
+ // plain `'active'` — identical to pre-#3464 behaviour.
158
+ currentPeriodEnd: subscription.currentPeriodEnd,
146
159
  trialEnd: null,
160
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
147
161
  seatModel: subscription.seatModel ?? void 0,
148
162
  memberRole: subscription.memberRole ?? void 0,
149
163
  organizationId: subscription.organizationId
@@ -837,7 +851,7 @@ function createProfileMethods(deps) {
837
851
  }
838
852
 
839
853
  // src/client/version-check.ts
840
- var SDK_VERSION = "5.6.0";
854
+ var SDK_VERSION = "5.8.0";
841
855
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
842
856
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
843
857
  function sdkHeaders() {
@@ -2190,6 +2204,7 @@ function createInquiryMethods(deps) {
2190
2204
  // src/subscriptions/userinfo-helpers.ts
2191
2205
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2192
2206
  "active",
2207
+ "active_canceling",
2193
2208
  "past_due_grace"
2194
2209
  ];
2195
2210
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2229,6 +2244,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2229
2244
  var DEFAULT_ALLOW_GRACE = true;
2230
2245
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2231
2246
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2247
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2248
+ blocked: "blocked",
2249
+ canceled: "canceled",
2250
+ expired: "expired",
2251
+ trial_expired: "trial_expired"
2252
+ };
2232
2253
  function resolveServiceAccessDenialReason(params, allowGrace) {
2233
2254
  if (params.hasAccess) {
2234
2255
  return null;
@@ -2239,10 +2260,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2239
2260
  if (params.isGrace && !allowGrace) {
2240
2261
  return "grace_disallowed";
2241
2262
  }
2242
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2263
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2243
2264
  return "blocked";
2244
2265
  }
2245
- return params.effectiveStatus;
2266
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2246
2267
  }
2247
2268
  function toServiceAccessDecision(response, allowGrace) {
2248
2269
  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-BhopNmnq.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-BhopNmnq.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-BYdtyO_2.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-DoGYvIll.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-DoGYvIll.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-BYdtyO_2.js';
6
6
 
7
7
  /** Token verification - verifyAccessToken() implementation */
8
8
 
@@ -124,7 +124,15 @@ function normalizeUserinfo(raw) {
124
124
  seatModel: raw.subscription.seat_model ?? null,
125
125
  memberRole: raw.subscription.member_role ?? null,
126
126
  organizationId: raw.subscription.organization_id ?? null,
127
- hasSeatAssignment: raw.subscription.has_seat_assignment ?? false
127
+ hasSeatAssignment: raw.subscription.has_seat_assignment ?? false,
128
+ // #3464: legacy FFID (< this issue) omits the field on the wire.
129
+ // The "unknown" axis is unused — no consumer differentiates "server
130
+ // didn't tell us" from "definitely not scheduled" because the
131
+ // companion `effectiveStatus` already returns `'active'` (not
132
+ // `'active_canceling'`) for legacy backends. Defaulting to `false`
133
+ // here keeps the downstream contract a plain boolean.
134
+ cancelAtPeriodEnd: raw.subscription.cancel_at_period_end ?? false,
135
+ currentPeriodEnd: raw.subscription.current_period_end ?? null
128
136
  } : void 0
129
137
  };
130
138
  }
@@ -141,8 +149,14 @@ function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
141
149
  planCode: subscription.planCode,
142
150
  planName: subscription.planCode,
143
151
  status: subscription.status,
144
- currentPeriodEnd: null,
152
+ // #3464: userinfo now carries `current_period_end` and
153
+ // `cancel_at_period_end`. Pre-#3464 FFID backends still omit them on
154
+ // the wire (`currentPeriodEnd === null` / `cancelAtPeriodEnd === false`
155
+ // after `normalizeUserinfo` defaults), so the session helper sees
156
+ // plain `'active'` — identical to pre-#3464 behaviour.
157
+ currentPeriodEnd: subscription.currentPeriodEnd,
145
158
  trialEnd: null,
159
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
146
160
  seatModel: subscription.seatModel ?? void 0,
147
161
  memberRole: subscription.memberRole ?? void 0,
148
162
  organizationId: subscription.organizationId
@@ -836,7 +850,7 @@ function createProfileMethods(deps) {
836
850
  }
837
851
 
838
852
  // src/client/version-check.ts
839
- var SDK_VERSION = "5.6.0";
853
+ var SDK_VERSION = "5.8.0";
840
854
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
841
855
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
842
856
  function sdkHeaders() {
@@ -2189,6 +2203,7 @@ function createInquiryMethods(deps) {
2189
2203
  // src/subscriptions/userinfo-helpers.ts
2190
2204
  var ACCESS_GRANTING_EFFECTIVE_STATUSES = [
2191
2205
  "active",
2206
+ "active_canceling",
2192
2207
  "past_due_grace"
2193
2208
  ];
2194
2209
  var BLOCKING_EFFECTIVE_STATUSES = [
@@ -2228,6 +2243,12 @@ var EXT_CHECK_ENDPOINT = "/api/v1/subscriptions/ext/check";
2228
2243
  var DEFAULT_ALLOW_GRACE = true;
2229
2244
  var DEFAULT_SERVICE_ACCESS_FAIL_POLICY = "failClosed";
2230
2245
  var SERVICE_ACCESS_DENIED_CODE = "SERVICE_ACCESS_DENIED";
2246
+ var DENIAL_REASON_BY_DENYING_STATUS = {
2247
+ blocked: "blocked",
2248
+ canceled: "canceled",
2249
+ expired: "expired",
2250
+ trial_expired: "trial_expired"
2251
+ };
2231
2252
  function resolveServiceAccessDenialReason(params, allowGrace) {
2232
2253
  if (params.hasAccess) {
2233
2254
  return null;
@@ -2238,10 +2259,10 @@ function resolveServiceAccessDenialReason(params, allowGrace) {
2238
2259
  if (params.isGrace && !allowGrace) {
2239
2260
  return "grace_disallowed";
2240
2261
  }
2241
- if (params.effectiveStatus === "active" || params.effectiveStatus === "past_due_grace") {
2262
+ if (ACCESS_GRANTING_EFFECTIVE_STATUSES.includes(params.effectiveStatus)) {
2242
2263
  return "blocked";
2243
2264
  }
2244
- return params.effectiveStatus;
2265
+ return DENIAL_REASON_BY_DENYING_STATUS[params.effectiveStatus];
2245
2266
  }
2246
2267
  function toServiceAccessDecision(response, allowGrace) {
2247
2268
  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-BhopNmnq.cjs';
2
+ import '../../types-BYdtyO_2.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-DoGYvIll.js';
2
+ import '../../types-BYdtyO_2.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,22 @@
29
33
  * This is the FFID-internal runtime-expired case (see
30
34
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
35
  *
36
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
37
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
38
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
39
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
40
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
41
+ * period is still live. SDK consumers receive the same literal whether
42
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
43
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
44
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
45
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
46
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
47
+ * original `'active'` verdict for legacy deployments.
48
+ *
32
49
  * @see /api/v1/subscriptions/ext/check
33
50
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
51
  */
35
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
52
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
53
 
37
54
  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,22 @@
29
33
  * This is the FFID-internal runtime-expired case (see
30
34
  * `getEffectiveSubscriptionStatus` on the server). Deny access.
31
35
  *
36
+ * **`active_canceling` is mirrored on the server (#3464, SDK 5.8.0).**
37
+ * Earlier (#3460, SDK 5.7.0) the literal was a session-only enrichment;
38
+ * since FFID #3464 the backend `EffectiveSubscriptionStatus` and both
39
+ * `/api/v1/subscriptions/ext/check` + `/oauth/userinfo` emit
40
+ * `active_canceling` directly when `cancel_at_period_end=true` and the
41
+ * period is still live. SDK consumers receive the same literal whether
42
+ * they read it from the session-derived `computeEffectiveStatusFromSession`
43
+ * or the server-derived `effectiveStatus` on the userinfo / ext-check
44
+ * paths. Pre-#3464 FFID backends omit `cancelAtPeriodEnd` on the wire; the
45
+ * SDK `normalizeUserinfo` boundary defaults it to `false`, so downstream
46
+ * consumers see a plain boolean and `computeEffectiveStatus` returns the
47
+ * original `'active'` verdict for legacy deployments.
48
+ *
32
49
  * @see /api/v1/subscriptions/ext/check
33
50
  * @see docs/03-implementation/EXPIRED_CONTRACT_HANDLING.md
34
51
  */
35
- type EffectiveSubscriptionStatus = 'active' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
52
+ type EffectiveSubscriptionStatus = 'active' | 'active_canceling' | 'past_due_grace' | 'blocked' | 'canceled' | 'trial_expired' | 'expired';
36
53
 
37
54
  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-BYdtyO_2.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-BYdtyO_2.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.8.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",