@feelflow/ffid-sdk 2.5.2 → 2.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.
@@ -1,5 +1,51 @@
1
1
  export { D as DEFAULT_API_BASE_URL } from '../constants-DvTGHPZn.cjs';
2
2
 
3
+ /**
4
+ * Newsletter types exposed by the FFID SDK.
5
+ *
6
+ * 2-layer newsletter model (#2078):
7
+ * - `inquiry_followup`: 問い合わせフォローアップ (Type A)
8
+ * - `general`: 定期ニュースレター (Type B)
9
+ *
10
+ * FFID account holders' preferences live in `user_marketing_preferences`;
11
+ * anonymous subscribers live in `newsletter_subscribers` with double
12
+ * opt-in required.
13
+ */
14
+ declare const FFID_NEWSLETTER_TYPES: readonly ["inquiry_followup", "general"];
15
+ type FFIDNewsletterType = (typeof FFID_NEWSLETTER_TYPES)[number];
16
+ interface FFIDNewsletterSubscribeParams {
17
+ /** Subscriber email address */
18
+ email: string;
19
+ /** One or more newsletter types to opt in to */
20
+ types: FFIDNewsletterType[];
21
+ /** Origin string recorded on the subscriber row (e.g. your service code) */
22
+ source: string;
23
+ /** ISO 639-1 locale (e.g. 'ja', 'en') */
24
+ locale?: string;
25
+ }
26
+ interface FFIDNewsletterSubscribeResponse {
27
+ ok: true;
28
+ /** True when a double opt-in confirmation email was sent */
29
+ requiresConfirmation: boolean;
30
+ /** True when the email matched an existing FFID account; the caller
31
+ * must sign in to update preferences. Preferences are NOT modified. */
32
+ requiresSignIn: boolean;
33
+ }
34
+ interface FFIDNewsletterConfirmParams {
35
+ /** Confirmation token received in the double opt-in email */
36
+ token: string;
37
+ }
38
+ interface FFIDNewsletterConfirmResponse {
39
+ ok: true;
40
+ }
41
+ interface FFIDNewsletterUnsubscribeParams {
42
+ /** Unsubscribe token received in the newsletter footer / List-Unsubscribe header */
43
+ token: string;
44
+ }
45
+ interface FFIDNewsletterUnsubscribeResponse {
46
+ ok: true;
47
+ }
48
+
3
49
  /** Cache adapter interface for FFID SDK token verification */
4
50
  /**
5
51
  * Pluggable cache adapter interface.
@@ -23,6 +69,11 @@ interface FFIDCacheConfig {
23
69
 
24
70
  /** Billing interval for subscriptions */
25
71
  type FFIDBillingInterval = 'monthly' | 'yearly';
72
+ /**
73
+ * Supported currencies for billing responses.
74
+ * Mirrors the platform's `SupportedCurrency` enum (`jpy` = zero-decimal Japanese Yen, `usd` = US Dollar).
75
+ */
76
+ type FFIDSupportedCurrency = 'jpy' | 'usd';
26
77
  /** Service information returned by plans endpoint */
27
78
  interface FFIDServiceInfo {
28
79
  id: string;
@@ -214,27 +265,56 @@ interface FFIDPlanChangeLineItem {
214
265
  description: string;
215
266
  amount: number;
216
267
  }
268
+ /** Fields shared by every plan-change preview variant (internal; consumers use `FFIDPlanChangePreview`) */
269
+ interface FFIDPlanChangePreviewBase {
270
+ /** Discriminant for preview response variants (reserved for future unions with e.g. seat-change previews) */
271
+ type: 'plan-change';
272
+ /** Current plan display info. `price` is the total for the current quantity (unitPrice × quantity), not per-seat. */
273
+ currentPlan: {
274
+ name: string;
275
+ price: number;
276
+ };
277
+ /** New plan display info. `price` is the total for the effective quantity (unitPrice × quantity), not per-seat. */
278
+ newPlan: {
279
+ name: string;
280
+ price: number;
281
+ };
282
+ billingInterval: FFIDBillingInterval;
283
+ isUpgrade: boolean;
284
+ /** Full amount of the next billing cycle at the new plan's standard price. Never overwritten by Stripe proration data. */
285
+ nextInvoiceAmount: number;
286
+ nextInvoiceDate: string | null;
287
+ currency: FFIDSupportedCurrency;
288
+ /** true when amounts are estimated from local prices (no Stripe proration data) */
289
+ isEstimate: boolean;
290
+ /** Reason why `isEstimate` is true. Only meaningful when `isEstimate === true`. */
291
+ estimateReason?: 'no_stripe_data' | 'custom_pricing' | 'stripe_error';
292
+ lineItems: FFIDPlanChangeLineItem[];
293
+ }
294
+ /**
295
+ * Plan change proration preview.
296
+ *
297
+ * `willApplyAtPeriodEnd` is a discriminant that constrains related fields at the type level:
298
+ * - `true`: period-end-deferred downgrade. `proratedAmount` is always 0; `effectiveDate` mirrors
299
+ * the subscription's `currentPeriodEnd` (may be `null` when the subscription has no active
300
+ * billing cycle yet — e.g. a pre-Stripe trial or a subscription whose billing cycle is unset).
301
+ * - `false`: immediate change. `effectiveDate` is always `null`; `proratedAmount` is the live
302
+ * (possibly Stripe-refined) day-basis difference.
303
+ */
304
+ type FFIDPlanChangePreview = FFIDPlanChangePreviewBase & ({
305
+ willApplyAtPeriodEnd: true;
306
+ effectiveDate: string | null;
307
+ /** 0 on period-end-deferred changes; charge happens at the next invoice */
308
+ proratedAmount: 0;
309
+ } | {
310
+ willApplyAtPeriodEnd: false;
311
+ effectiveDate: null;
312
+ /** Prorated difference charged within the current billing period */
313
+ proratedAmount: number;
314
+ });
217
315
  /** Response from plan change preview endpoint */
218
316
  interface FFIDPlanChangePreviewResponse {
219
- preview: {
220
- currentPlan: {
221
- name: string;
222
- price: number;
223
- };
224
- newPlan: {
225
- name: string;
226
- price: number;
227
- };
228
- billingInterval: FFIDBillingInterval;
229
- isUpgrade: boolean;
230
- proratedAmount: number;
231
- nextInvoiceAmount: number;
232
- nextInvoiceDate: string | null;
233
- currency: string;
234
- isEstimate: boolean;
235
- estimateReason?: string;
236
- lineItems: FFIDPlanChangeLineItem[];
237
- };
317
+ preview: FFIDPlanChangePreview;
238
318
  }
239
319
 
240
320
  /**
@@ -745,6 +825,12 @@ declare function createFFIDClient(config: FFIDConfig): {
745
825
  accessToken: string;
746
826
  refreshToken: string;
747
827
  }) => Promise<FFIDApiResponse<FFIDOtpVerifyResponse>>;
828
+ /** Newsletter methods (subscribe / confirm / unsubscribe) */
829
+ newsletter: {
830
+ subscribe: (params: FFIDNewsletterSubscribeParams) => Promise<FFIDApiResponse<FFIDNewsletterSubscribeResponse>>;
831
+ confirm: (params: FFIDNewsletterConfirmParams) => Promise<FFIDApiResponse<FFIDNewsletterConfirmResponse>>;
832
+ unsubscribe: (params: FFIDNewsletterUnsubscribeParams) => Promise<FFIDApiResponse<FFIDNewsletterUnsubscribeResponse>>;
833
+ };
748
834
  /** Token store (token mode only) */
749
835
  tokenStore: TokenStore;
750
836
  /** Resolved auth mode */
@@ -1,5 +1,51 @@
1
1
  export { D as DEFAULT_API_BASE_URL } from '../constants-DvTGHPZn.js';
2
2
 
3
+ /**
4
+ * Newsletter types exposed by the FFID SDK.
5
+ *
6
+ * 2-layer newsletter model (#2078):
7
+ * - `inquiry_followup`: 問い合わせフォローアップ (Type A)
8
+ * - `general`: 定期ニュースレター (Type B)
9
+ *
10
+ * FFID account holders' preferences live in `user_marketing_preferences`;
11
+ * anonymous subscribers live in `newsletter_subscribers` with double
12
+ * opt-in required.
13
+ */
14
+ declare const FFID_NEWSLETTER_TYPES: readonly ["inquiry_followup", "general"];
15
+ type FFIDNewsletterType = (typeof FFID_NEWSLETTER_TYPES)[number];
16
+ interface FFIDNewsletterSubscribeParams {
17
+ /** Subscriber email address */
18
+ email: string;
19
+ /** One or more newsletter types to opt in to */
20
+ types: FFIDNewsletterType[];
21
+ /** Origin string recorded on the subscriber row (e.g. your service code) */
22
+ source: string;
23
+ /** ISO 639-1 locale (e.g. 'ja', 'en') */
24
+ locale?: string;
25
+ }
26
+ interface FFIDNewsletterSubscribeResponse {
27
+ ok: true;
28
+ /** True when a double opt-in confirmation email was sent */
29
+ requiresConfirmation: boolean;
30
+ /** True when the email matched an existing FFID account; the caller
31
+ * must sign in to update preferences. Preferences are NOT modified. */
32
+ requiresSignIn: boolean;
33
+ }
34
+ interface FFIDNewsletterConfirmParams {
35
+ /** Confirmation token received in the double opt-in email */
36
+ token: string;
37
+ }
38
+ interface FFIDNewsletterConfirmResponse {
39
+ ok: true;
40
+ }
41
+ interface FFIDNewsletterUnsubscribeParams {
42
+ /** Unsubscribe token received in the newsletter footer / List-Unsubscribe header */
43
+ token: string;
44
+ }
45
+ interface FFIDNewsletterUnsubscribeResponse {
46
+ ok: true;
47
+ }
48
+
3
49
  /** Cache adapter interface for FFID SDK token verification */
4
50
  /**
5
51
  * Pluggable cache adapter interface.
@@ -23,6 +69,11 @@ interface FFIDCacheConfig {
23
69
 
24
70
  /** Billing interval for subscriptions */
25
71
  type FFIDBillingInterval = 'monthly' | 'yearly';
72
+ /**
73
+ * Supported currencies for billing responses.
74
+ * Mirrors the platform's `SupportedCurrency` enum (`jpy` = zero-decimal Japanese Yen, `usd` = US Dollar).
75
+ */
76
+ type FFIDSupportedCurrency = 'jpy' | 'usd';
26
77
  /** Service information returned by plans endpoint */
27
78
  interface FFIDServiceInfo {
28
79
  id: string;
@@ -214,27 +265,56 @@ interface FFIDPlanChangeLineItem {
214
265
  description: string;
215
266
  amount: number;
216
267
  }
268
+ /** Fields shared by every plan-change preview variant (internal; consumers use `FFIDPlanChangePreview`) */
269
+ interface FFIDPlanChangePreviewBase {
270
+ /** Discriminant for preview response variants (reserved for future unions with e.g. seat-change previews) */
271
+ type: 'plan-change';
272
+ /** Current plan display info. `price` is the total for the current quantity (unitPrice × quantity), not per-seat. */
273
+ currentPlan: {
274
+ name: string;
275
+ price: number;
276
+ };
277
+ /** New plan display info. `price` is the total for the effective quantity (unitPrice × quantity), not per-seat. */
278
+ newPlan: {
279
+ name: string;
280
+ price: number;
281
+ };
282
+ billingInterval: FFIDBillingInterval;
283
+ isUpgrade: boolean;
284
+ /** Full amount of the next billing cycle at the new plan's standard price. Never overwritten by Stripe proration data. */
285
+ nextInvoiceAmount: number;
286
+ nextInvoiceDate: string | null;
287
+ currency: FFIDSupportedCurrency;
288
+ /** true when amounts are estimated from local prices (no Stripe proration data) */
289
+ isEstimate: boolean;
290
+ /** Reason why `isEstimate` is true. Only meaningful when `isEstimate === true`. */
291
+ estimateReason?: 'no_stripe_data' | 'custom_pricing' | 'stripe_error';
292
+ lineItems: FFIDPlanChangeLineItem[];
293
+ }
294
+ /**
295
+ * Plan change proration preview.
296
+ *
297
+ * `willApplyAtPeriodEnd` is a discriminant that constrains related fields at the type level:
298
+ * - `true`: period-end-deferred downgrade. `proratedAmount` is always 0; `effectiveDate` mirrors
299
+ * the subscription's `currentPeriodEnd` (may be `null` when the subscription has no active
300
+ * billing cycle yet — e.g. a pre-Stripe trial or a subscription whose billing cycle is unset).
301
+ * - `false`: immediate change. `effectiveDate` is always `null`; `proratedAmount` is the live
302
+ * (possibly Stripe-refined) day-basis difference.
303
+ */
304
+ type FFIDPlanChangePreview = FFIDPlanChangePreviewBase & ({
305
+ willApplyAtPeriodEnd: true;
306
+ effectiveDate: string | null;
307
+ /** 0 on period-end-deferred changes; charge happens at the next invoice */
308
+ proratedAmount: 0;
309
+ } | {
310
+ willApplyAtPeriodEnd: false;
311
+ effectiveDate: null;
312
+ /** Prorated difference charged within the current billing period */
313
+ proratedAmount: number;
314
+ });
217
315
  /** Response from plan change preview endpoint */
218
316
  interface FFIDPlanChangePreviewResponse {
219
- preview: {
220
- currentPlan: {
221
- name: string;
222
- price: number;
223
- };
224
- newPlan: {
225
- name: string;
226
- price: number;
227
- };
228
- billingInterval: FFIDBillingInterval;
229
- isUpgrade: boolean;
230
- proratedAmount: number;
231
- nextInvoiceAmount: number;
232
- nextInvoiceDate: string | null;
233
- currency: string;
234
- isEstimate: boolean;
235
- estimateReason?: string;
236
- lineItems: FFIDPlanChangeLineItem[];
237
- };
317
+ preview: FFIDPlanChangePreview;
238
318
  }
239
319
 
240
320
  /**
@@ -745,6 +825,12 @@ declare function createFFIDClient(config: FFIDConfig): {
745
825
  accessToken: string;
746
826
  refreshToken: string;
747
827
  }) => Promise<FFIDApiResponse<FFIDOtpVerifyResponse>>;
828
+ /** Newsletter methods (subscribe / confirm / unsubscribe) */
829
+ newsletter: {
830
+ subscribe: (params: FFIDNewsletterSubscribeParams) => Promise<FFIDApiResponse<FFIDNewsletterSubscribeResponse>>;
831
+ confirm: (params: FFIDNewsletterConfirmParams) => Promise<FFIDApiResponse<FFIDNewsletterConfirmResponse>>;
832
+ unsubscribe: (params: FFIDNewsletterUnsubscribeParams) => Promise<FFIDApiResponse<FFIDNewsletterUnsubscribeResponse>>;
833
+ };
748
834
  /** Token store (token mode only) */
749
835
  tokenStore: TokenStore;
750
836
  /** Resolved auth mode */
@@ -98,9 +98,15 @@ function createTokenStore(storageType) {
98
98
  }
99
99
 
100
100
  // src/client/oauth-userinfo.ts
101
- var VALID_SUBSCRIPTION_STATUSES = ["trialing", "active", "past_due", "canceled", "paused"];
102
- function isValidSubscriptionStatus(value) {
103
- return VALID_SUBSCRIPTION_STATUSES.includes(value);
101
+ var SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES = [
102
+ "trialing",
103
+ "active",
104
+ "past_due",
105
+ "canceled",
106
+ "paused"
107
+ ];
108
+ function isSessionEligibleSubscriptionStatus(value) {
109
+ return typeof value === "string" && SESSION_ELIGIBLE_SUBSCRIPTION_STATUSES.includes(value);
104
110
  }
105
111
  function normalizeUserinfo(raw) {
106
112
  return {
@@ -126,7 +132,7 @@ function normalizeUserinfo(raw) {
126
132
  }
127
133
  function mapUserinfoSubscriptionToSession(userinfo, serviceCode) {
128
134
  const subscription = userinfo.subscription;
129
- if (!subscription || !subscription.planCode || !isValidSubscriptionStatus(subscription.status)) {
135
+ if (!subscription || !subscription.planCode || !isSessionEligibleSubscriptionStatus(subscription.status)) {
130
136
  return [];
131
137
  }
132
138
  return [
@@ -610,7 +616,7 @@ function createMembersMethods(deps) {
610
616
  }
611
617
 
612
618
  // src/client/version-check.ts
613
- var SDK_VERSION = "2.5.2";
619
+ var SDK_VERSION = "2.7.0";
614
620
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
615
621
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
616
622
  function sdkHeaders() {
@@ -1541,6 +1547,118 @@ function createContractWizardMethods(deps) {
1541
1547
  };
1542
1548
  }
1543
1549
 
1550
+ // src/newsletter/ffid-newsletter-client.ts
1551
+ var EXT_SUBSCRIBE_ENDPOINT2 = "/api/v1/ext/newsletter/subscribe";
1552
+ var CONFIRM_ENDPOINT = "/api/newsletter/confirm";
1553
+ var UNSUBSCRIBE_ENDPOINT = "/api/newsletter/unsubscribe";
1554
+ function trimOrEmpty(s) {
1555
+ return typeof s === "string" ? s.trim() : "";
1556
+ }
1557
+ async function postPublic(url, init, opts) {
1558
+ let response;
1559
+ try {
1560
+ response = await fetch(url, init);
1561
+ } catch (err) {
1562
+ return {
1563
+ error: opts.createError(
1564
+ "NETWORK_ERROR",
1565
+ err instanceof Error ? err.message : "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F"
1566
+ )
1567
+ };
1568
+ }
1569
+ if (!response.ok) {
1570
+ try {
1571
+ const body = await response.json();
1572
+ if (body?.error?.code || body?.error?.message) {
1573
+ return {
1574
+ error: opts.createError(
1575
+ body.error.code ?? "UNKNOWN_ERROR",
1576
+ body.error.message ?? `${opts.fallbackMessage} (status: ${response.status})`
1577
+ )
1578
+ };
1579
+ }
1580
+ } catch {
1581
+ }
1582
+ return {
1583
+ error: opts.createError(
1584
+ "NETWORK_ERROR",
1585
+ `${opts.fallbackMessage} (status: ${response.status})`
1586
+ )
1587
+ };
1588
+ }
1589
+ return { data: opts.success };
1590
+ }
1591
+ function createNewsletterMethods(deps) {
1592
+ const { fetchWithAuth, baseUrl, createError } = deps;
1593
+ async function subscribe(params) {
1594
+ const email = trimOrEmpty(params.email);
1595
+ const source = trimOrEmpty(params.source);
1596
+ if (!email) {
1597
+ return { error: createError("VALIDATION_ERROR", "email \u306F\u5FC5\u9808\u3067\u3059") };
1598
+ }
1599
+ if (!source) {
1600
+ return { error: createError("VALIDATION_ERROR", "source \u306F\u5FC5\u9808\u3067\u3059") };
1601
+ }
1602
+ if (!Array.isArray(params.types) || params.types.length === 0) {
1603
+ return {
1604
+ error: createError(
1605
+ "VALIDATION_ERROR",
1606
+ "types \u306B\u306F1\u3064\u4EE5\u4E0A\u306E newsletter \u7A2E\u5225\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
1607
+ )
1608
+ };
1609
+ }
1610
+ return fetchWithAuth(
1611
+ EXT_SUBSCRIBE_ENDPOINT2,
1612
+ {
1613
+ method: "POST",
1614
+ body: JSON.stringify({
1615
+ email,
1616
+ types: params.types,
1617
+ source,
1618
+ locale: params.locale
1619
+ })
1620
+ }
1621
+ );
1622
+ }
1623
+ async function confirm(params) {
1624
+ const token = trimOrEmpty(params.token);
1625
+ if (!token) {
1626
+ return { error: createError("VALIDATION_ERROR", "token \u306F\u5FC5\u9808\u3067\u3059") };
1627
+ }
1628
+ return postPublic(
1629
+ `${baseUrl}${CONFIRM_ENDPOINT}`,
1630
+ {
1631
+ method: "POST",
1632
+ headers: { "Content-Type": "application/json" },
1633
+ body: JSON.stringify({ token })
1634
+ },
1635
+ {
1636
+ success: { ok: true },
1637
+ createError,
1638
+ fallbackMessage: "\u78BA\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5931\u6557\u3057\u307E\u3057\u305F"
1639
+ }
1640
+ );
1641
+ }
1642
+ async function unsubscribe(params) {
1643
+ const token = trimOrEmpty(params.token);
1644
+ if (!token) {
1645
+ return { error: createError("VALIDATION_ERROR", "token \u306F\u5FC5\u9808\u3067\u3059") };
1646
+ }
1647
+ const url = new URL(`${baseUrl}${UNSUBSCRIBE_ENDPOINT}`);
1648
+ url.searchParams.set("token", token);
1649
+ return postPublic(
1650
+ url.toString(),
1651
+ { method: "POST" },
1652
+ {
1653
+ success: { ok: true },
1654
+ createError,
1655
+ fallbackMessage: "\u89E3\u9664\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5931\u6557\u3057\u307E\u3057\u305F"
1656
+ }
1657
+ );
1658
+ }
1659
+ return { subscribe, confirm, unsubscribe };
1660
+ }
1661
+
1544
1662
  // src/client/ffid-client.ts
1545
1663
  var UNAUTHORIZED_STATUS2 = 401;
1546
1664
  var SDK_LOG_PREFIX = "[FFID SDK]";
@@ -1816,6 +1934,11 @@ function createFFIDClient(config) {
1816
1934
  createError,
1817
1935
  errorCodes: FFID_ERROR_CODES
1818
1936
  });
1937
+ const newsletter = createNewsletterMethods({
1938
+ fetchWithAuth,
1939
+ baseUrl,
1940
+ createError
1941
+ });
1819
1942
  const verifyAccessToken = createVerifyAccessToken({
1820
1943
  authMode,
1821
1944
  baseUrl,
@@ -1870,6 +1993,8 @@ function createFFIDClient(config) {
1870
1993
  confirmPasswordReset,
1871
1994
  sendOtp,
1872
1995
  verifyOtp,
1996
+ /** Newsletter methods (subscribe / confirm / unsubscribe) */
1997
+ newsletter,
1873
1998
  /** Token store (token mode only) */
1874
1999
  tokenStore,
1875
2000
  /** Resolved auth mode */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "2.5.2",
3
+ "version": "2.7.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
5
  "keywords": [
6
6
  "feelflow",
@@ -97,6 +97,7 @@
97
97
  "devDependencies": {
98
98
  "@testing-library/jest-dom": "^6.0.0",
99
99
  "@testing-library/react": "^16.0.0",
100
+ "@types/node": "^22.19.17",
100
101
  "@types/react": "^19.0.0",
101
102
  "@types/react-dom": "^19.0.0",
102
103
  "@typescript-eslint/eslint-plugin": "^8.58.0",