@feelflow/ffid-sdk 5.0.2 → 5.2.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.
@@ -165,10 +165,9 @@ declare const FFIDConsentCategoryMetadataSchema: z.ZodObject<{
165
165
  *
166
166
  * `publishedAt` accepts both `Z` suffix and numeric offsets (`+00:00`, `+09:00`)
167
167
  * so that consumers tolerate Postgres `timestamptz` raw passthrough as well as
168
- * `Z`-normalized output. The FFID server normalizes to `Z` since Issue #3244
169
- * (see `src/lib/cookie-consent/serialize.ts#isoZ`), but third-party / future
170
- * server paths may emit offsets — tolerant parsing is the defense-in-depth
171
- * layer specified in Issue #3245.
168
+ * `Z`-normalized output. The FFID server normalizes to `Z` since Issue #3244;
169
+ * third-party / future server paths may emit offsets — tolerant parsing is
170
+ * the defense-in-depth layer specified in Issue #3245.
172
171
  */
173
172
  declare const FFIDCookiePolicySchema: z.ZodObject<{
174
173
  id: z.ZodString;
@@ -478,6 +477,16 @@ declare const CONSENT_COOKIE_MAX_AGE_SEC: number;
478
477
  * first, then fall back to cookie. See `FFIDAnalyticsProvider.bootstrap`.
479
478
  */
480
479
  declare const CONSENT_SESSION_STORAGE_KEY = "ffid_consent_state";
480
+ /**
481
+ * sessionStorage key for the dismissal timestamp ("あとで" cooldown).
482
+ *
483
+ * EDPB Guidelines 05/2020 §86 forbids treating dismissal as a refusal, so we
484
+ * never write this to a cookie or DB. Holding the timestamp in sessionStorage
485
+ * keeps it scoped to the current tab session — once the tab closes, the user
486
+ * is reprompted on next visit. See `FFIDAnalyticsProvider`'s
487
+ * `dismissalCooldownMinutes` prop for the auto-show suppression policy.
488
+ */
489
+ declare const CONSENT_DISMISSAL_TIMESTAMP_KEY = "ffid_consent_dismissed_at";
481
490
  interface WriteConsentCookieOptions {
482
491
  /** Override Secure flag for non-https custom domains. Defaults to production. */
483
492
  secure?: boolean;
@@ -521,6 +530,34 @@ declare function deleteConsentCookie(opts?: WriteConsentCookieOptions): boolean;
521
530
  declare function readConsentSessionMirror(): FFIDConsentCategories | null;
522
531
  declare function writeConsentSessionMirror(cats: FFIDConsentCategories): boolean;
523
532
  declare function clearConsentSessionMirror(): void;
533
+ /**
534
+ * Read the millisecond unix timestamp at which the banner was last dismissed,
535
+ * or `null` if absent / invalid / sessionStorage unavailable.
536
+ *
537
+ * Defensive parsing rejects non-finite values, negatives, and future-dated
538
+ * timestamps (clock-skew / tampering). When a corrupted value is detected,
539
+ * the helper **auto-heals** by clearing the slot — otherwise the same bad
540
+ * value would be re-parsed on every Provider mount and a buggy / extension-
541
+ * polluted consumer site could leave the cooldown effectively permanently
542
+ * disabled with no surface signal.
543
+ */
544
+ declare function readConsentDismissalTimestamp(): number | null;
545
+ /**
546
+ * Write the dismissal timestamp. Returns `true` on success, `false` when
547
+ * sessionStorage is unavailable / throws (e.g., quota, old Safari Private
548
+ * Browsing) **or** when `nowMs` is non-finite / negative.
549
+ *
550
+ * Callers may safely ignore the return — failure degrades to the legacy
551
+ * "reprompt every Provider mount" behavior, which is the published v5.1.0
552
+ * baseline. Dismissal carries no GDPR weight (EDPB §86) so there is no
553
+ * obligation to surface storage failure via `onError`.
554
+ */
555
+ declare function writeConsentDismissalTimestamp(nowMs?: number): boolean;
556
+ /**
557
+ * Idempotent clear of the dismissal timestamp. Safe to call when no value is
558
+ * present or sessionStorage is unavailable; never throws.
559
+ */
560
+ declare function clearConsentDismissalTimestamp(): void;
524
561
 
525
562
  /**
526
563
  * Cookie Consent — Google Consent Mode v2 wiring.
@@ -653,11 +690,29 @@ interface FFIDAnalyticsProviderProps {
653
690
  onError?: (error: FFIDConsentError) => void;
654
691
  /** Whether to open banner automatically when no decision is on file. Default: true. */
655
692
  autoShowBanner?: boolean;
693
+ /**
694
+ * Minutes to suppress auto-reopen after the user presses "あとで" (closeBanner).
695
+ *
696
+ * The dismissal timestamp is held in `sessionStorage` only — never written
697
+ * to cookie or DB — so it expires when the tab closes. This is EDPB
698
+ * Guidelines 05/2020 §86 compliant: dismissal is not a refusal, and the
699
+ * user is reprompted on the next fresh visit regardless of cooldown.
700
+ *
701
+ * Accepts any `number`; non-finite (`NaN` / `±Infinity`) and negative
702
+ * values are treated as `0` (suppression disabled). `0` reverts to the
703
+ * legacy v5.1.0 "reprompt on every Provider mount" behavior. Default: 30.
704
+ *
705
+ * Manual `openBanner()` calls ignore the cooldown so consumer "Cookie 設定"
706
+ * links keep working, and `needsRenewal=true` overrides the cooldown so the
707
+ * renewal reprompt (CNIL/ICO renewal guidance — see PR #3261) is never
708
+ * suppressed by a UX dismissal.
709
+ */
710
+ dismissalCooldownMinutes?: number;
656
711
  /** Optional fetch override (mainly for tests). */
657
712
  fetchImpl?: typeof fetch;
658
713
  children: ReactNode;
659
714
  }
660
- declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
715
+ declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
661
716
 
662
717
  interface FFIDConsentContextValue {
663
718
  /** Current consent state. Always non-null; "not yet decided" is encoded by `hasDecided=false`. */
@@ -797,15 +852,23 @@ declare function useFFIDConsentPreferences(): UseFFIDConsentPreferencesReturn;
797
852
  * `<FFIDCookieBanner>` — bottom-sheet consent banner.
798
853
  *
799
854
  * Renders only when `state.hasDecided === false` or `state.needsRenewal === true`.
800
- * Three buttons (spec §6.2):
855
+ * Four buttons (spec §6.2):
801
856
  * 1. "すべて同意" (Accept all)
802
- * 2. "必要なものだけ" (Necessary only)
857
+ * 2. "同意しない" (Necessary only — `necessary=true` is enforced by ePrivacy
858
+ * 5(3) and the DB invariant; the 3 user-toggleable categories are
859
+ * recorded as false. The action itself is preserved in `source='banner'`)
803
860
  * 3. "詳細設定" (Open preference center)
861
+ * 4. "あとで" (Dismiss without recording a decision — banner re-appears on
862
+ * the next visit because no row is written)
804
863
  *
805
864
  * GDPR Article 7(1)/(2) UX requirements:
806
865
  * - "同意" and "拒否" share the same size + visual prominence.
807
866
  * - Buttons share the same styles by default; consumers can override via
808
- * `classNames` props (`primaryButton`, `secondaryButton`, `linkButton`).
867
+ * `classNames` props (`primaryButton`, `secondaryButton`, `linkButton`,
868
+ * `closeButton`).
869
+ * - "あとで" is intentionally not a refusal: EDPB Guidelines 05/2020 §86
870
+ * forbid treating dismissal as a refusal. We honor that by writing
871
+ * nothing on dismissal and re-prompting next visit.
809
872
  *
810
873
  * Refs: docs/02-design/COOKIE_CONSENT.md §6.2
811
874
  */
@@ -817,6 +880,7 @@ interface FFIDCookieBannerClassNames {
817
880
  primaryButton?: string;
818
881
  secondaryButton?: string;
819
882
  linkButton?: string;
883
+ closeButton?: string;
820
884
  }
821
885
  interface FFIDCookieBannerProps {
822
886
  /** Root container class. */
@@ -827,14 +891,40 @@ interface FFIDCookieBannerProps {
827
891
  message?: ReactNode;
828
892
  /** Accept-all button label. Default: "すべて同意". */
829
893
  acceptAllLabel?: ReactNode;
830
- /** Necessary-only button label. Default: "必要なものだけ". */
894
+ /**
895
+ * Necessary-only button label. Default: "同意しない".
896
+ *
897
+ * Selecting this button writes `necessary=true, functional=false,
898
+ * analytics=false, marketing=false` to the audit trail with
899
+ * `source='banner'`. The `necessary=true` value is enforced by the DB
900
+ * invariant; ePrivacy 5(3) exempts strictly necessary cookies from consent.
901
+ * Consumers that want to surface the legacy "必要なものだけ" wording can
902
+ * override via this prop.
903
+ */
831
904
  necessaryOnlyLabel?: ReactNode;
832
905
  /** Preference center link label. Default: "詳細設定". */
833
906
  preferencesLabel?: ReactNode;
907
+ /**
908
+ * Dismiss-without-recording button label. Default: "あとで".
909
+ *
910
+ * Selecting this button calls `closeBanner()` — session-only state, no row
911
+ * is written to the database. The banner re-appears on the next visit
912
+ * because bootstrap still sees `hasDecided=false`. Aligns with EDPB
913
+ * Guidelines 05/2020 §86 prohibiting treating dismissal as a refusal.
914
+ *
915
+ * NOTE for consumers: do NOT pass `closeLabel={null}` or `closeLabel=""`
916
+ * to visually hide the button while keeping the others. Doing so leaves
917
+ * a click-able empty area and breaks the GDPR Article 7(2) requirement
918
+ * that "consent" and "refusal" / "dismissal" controls share equal visual
919
+ * prominence. If you need to suppress dismissal entirely (e.g., for a
920
+ * strict-consent jurisdiction), build a custom banner using
921
+ * `useFFIDConsent()` instead of overriding the label.
922
+ */
923
+ closeLabel?: ReactNode;
834
924
  /** Custom root style. */
835
925
  style?: CSSProperties;
836
926
  }
837
- declare function FFIDCookieBanner({ className, classNames, message, acceptAllLabel, necessaryOnlyLabel, preferencesLabel, style, }: FFIDCookieBannerProps): React.JSX.Element | null;
927
+ declare function FFIDCookieBanner({ className, classNames, message, acceptAllLabel, necessaryOnlyLabel, preferencesLabel, closeLabel, style, }: FFIDCookieBannerProps): React.JSX.Element | null;
838
928
 
839
929
  /**
840
930
  * `<FFIDCookieSettings>` — preference center modal.
@@ -882,4 +972,4 @@ interface FFIDCookieLinkProps {
882
972
  }
883
973
  declare function FFIDCookieLink({ children, className, style, id, }: FFIDCookieLinkProps): React.JSX.Element;
884
974
 
885
- export { GetCategoriesWireSchema as $, ALL_DENIED_EXCEPT_NECESSARY as A, FFIDConsentMergeWarningSchema as B, CONSENT_COOKIE_MAX_AGE_SEC as C, DEFAULT_CONSENT_ERROR_MESSAGES as D, type FFIDConsentSource as E, type FFIDConsentResult as F, type GetConsentMeWire as G, FFIDConsentSourceSchema as H, FFIDConsentStateSchema as I, FFIDConsentUpdateSchema as J, FFIDCookieBanner as K, type FFIDCookieBannerClassNames as L, type FFIDCookieBannerProps as M, FFIDCookieLink as N, type FFIDCookieLinkProps as O, type PostConsentWire as P, FFIDCookiePolicySchema as Q, FFIDCookieSettings as R, type FFIDCookieSettingsClassNames as S, type FFIDCookieSettingsProps as T, type FFIDInternalConsentSource as U, FFIDInternalConsentSourceSchema as V, type FFIDPublicConsentSource as W, FFIDPublicConsentSourceSchema as X, FFID_CONSENT_ERROR_CODES as Y, FFID_CONSENT_NOT_DECIDED_STATE as Z, type GetCategoriesWire as _, type FFIDConsentState as a, GetConsentMeWireSchema as a0, type GetDocumentWire as a1, GetDocumentWireSchema as a2, type GtagBridge as a3, type GtagBridgeOptions as a4, type PostConsentRequest as a5, PostConsentRequestSchema as a6, PostConsentWireSchema as a7, type PostSyncRequest as a8, PostSyncRequestSchema as a9, PostSyncWireSchema as aa, PostWithdrawWireSchema as ab, type UseFFIDConsentPreferencesReturn as ac, type UseFFIDConsentReturn as ad, clearConsentSessionMirror as ae, createGtagBridge as af, decodeConsentCookie as ag, deleteConsentCookie as ah, encodeConsentCookie as ai, mapCategoriesToGtagParams as aj, readConsentCookie as ak, readConsentSessionMirror as al, useFFIDConsent as am, useFFIDConsentPreferences as an, writeConsentCookie as ao, writeConsentSessionMirror as ap, type FFIDConsentUpdate as b, type FFIDConsentCategories as c, type FFIDConsentSyncResult as d, type FFIDConsentCategoryMetadata as e, type FFIDCookiePolicy as f, type PostSyncWire as g, type PostWithdrawWire as h, CONSENT_COOKIE_NAME as i, CONSENT_SESSION_STORAGE_KEY as j, COOKIE_VERSION as k, DEFAULT_MERGE_WARNING_MESSAGES as l, DeviceIdSchema as m, FFIDAnalyticsProvider as n, type FFIDAnalyticsProviderProps as o, FFIDConsentCategoriesSchema as p, type FFIDConsentCategoryCode as q, FFIDConsentCategoryCodeSchema as r, FFIDConsentCategoryMetadataSchema as s, FFIDConsentContext as t, type FFIDConsentContextValue as u, FFIDConsentError as v, type FFIDConsentErrorCode as w, type FFIDConsentMergeStrategy as x, FFIDConsentMergeStrategySchema as y, type FFIDConsentMergeWarning as z };
975
+ export { type GetCategoriesWire as $, ALL_DENIED_EXCEPT_NECESSARY as A, type FFIDConsentMergeWarning as B, CONSENT_COOKIE_MAX_AGE_SEC as C, DEFAULT_CONSENT_ERROR_MESSAGES as D, FFIDConsentMergeWarningSchema as E, type FFIDConsentResult as F, type GetConsentMeWire as G, type FFIDConsentSource as H, FFIDConsentSourceSchema as I, FFIDConsentStateSchema as J, FFIDConsentUpdateSchema as K, FFIDCookieBanner as L, type FFIDCookieBannerClassNames as M, type FFIDCookieBannerProps as N, FFIDCookieLink as O, type PostConsentWire as P, type FFIDCookieLinkProps as Q, FFIDCookiePolicySchema as R, FFIDCookieSettings as S, type FFIDCookieSettingsClassNames as T, type FFIDCookieSettingsProps as U, type FFIDInternalConsentSource as V, FFIDInternalConsentSourceSchema as W, type FFIDPublicConsentSource as X, FFIDPublicConsentSourceSchema as Y, FFID_CONSENT_ERROR_CODES as Z, FFID_CONSENT_NOT_DECIDED_STATE as _, type FFIDConsentState as a, GetCategoriesWireSchema as a0, GetConsentMeWireSchema as a1, type GetDocumentWire as a2, GetDocumentWireSchema as a3, type GtagBridge as a4, type GtagBridgeOptions as a5, type PostConsentRequest as a6, PostConsentRequestSchema as a7, PostConsentWireSchema as a8, type PostSyncRequest as a9, PostSyncRequestSchema as aa, PostSyncWireSchema as ab, PostWithdrawWireSchema as ac, type UseFFIDConsentPreferencesReturn as ad, type UseFFIDConsentReturn as ae, clearConsentDismissalTimestamp as af, clearConsentSessionMirror as ag, createGtagBridge as ah, decodeConsentCookie as ai, deleteConsentCookie as aj, encodeConsentCookie as ak, mapCategoriesToGtagParams as al, readConsentCookie as am, readConsentDismissalTimestamp as an, readConsentSessionMirror as ao, useFFIDConsent as ap, useFFIDConsentPreferences as aq, writeConsentCookie as ar, writeConsentDismissalTimestamp as as, writeConsentSessionMirror as at, type FFIDConsentUpdate as b, type FFIDConsentCategories as c, type FFIDConsentSyncResult as d, type FFIDConsentCategoryMetadata as e, type FFIDCookiePolicy as f, type PostSyncWire as g, type PostWithdrawWire as h, CONSENT_COOKIE_NAME as i, CONSENT_DISMISSAL_TIMESTAMP_KEY as j, CONSENT_SESSION_STORAGE_KEY as k, COOKIE_VERSION as l, DEFAULT_MERGE_WARNING_MESSAGES as m, DeviceIdSchema as n, FFIDAnalyticsProvider as o, type FFIDAnalyticsProviderProps as p, FFIDConsentCategoriesSchema as q, type FFIDConsentCategoryCode as r, FFIDConsentCategoryCodeSchema as s, FFIDConsentCategoryMetadataSchema as t, FFIDConsentContext as u, type FFIDConsentContextValue as v, FFIDConsentError as w, type FFIDConsentErrorCode as x, type FFIDConsentMergeStrategy as y, FFIDConsentMergeStrategySchema as z };
@@ -165,10 +165,9 @@ declare const FFIDConsentCategoryMetadataSchema: z.ZodObject<{
165
165
  *
166
166
  * `publishedAt` accepts both `Z` suffix and numeric offsets (`+00:00`, `+09:00`)
167
167
  * so that consumers tolerate Postgres `timestamptz` raw passthrough as well as
168
- * `Z`-normalized output. The FFID server normalizes to `Z` since Issue #3244
169
- * (see `src/lib/cookie-consent/serialize.ts#isoZ`), but third-party / future
170
- * server paths may emit offsets — tolerant parsing is the defense-in-depth
171
- * layer specified in Issue #3245.
168
+ * `Z`-normalized output. The FFID server normalizes to `Z` since Issue #3244;
169
+ * third-party / future server paths may emit offsets — tolerant parsing is
170
+ * the defense-in-depth layer specified in Issue #3245.
172
171
  */
173
172
  declare const FFIDCookiePolicySchema: z.ZodObject<{
174
173
  id: z.ZodString;
@@ -478,6 +477,16 @@ declare const CONSENT_COOKIE_MAX_AGE_SEC: number;
478
477
  * first, then fall back to cookie. See `FFIDAnalyticsProvider.bootstrap`.
479
478
  */
480
479
  declare const CONSENT_SESSION_STORAGE_KEY = "ffid_consent_state";
480
+ /**
481
+ * sessionStorage key for the dismissal timestamp ("あとで" cooldown).
482
+ *
483
+ * EDPB Guidelines 05/2020 §86 forbids treating dismissal as a refusal, so we
484
+ * never write this to a cookie or DB. Holding the timestamp in sessionStorage
485
+ * keeps it scoped to the current tab session — once the tab closes, the user
486
+ * is reprompted on next visit. See `FFIDAnalyticsProvider`'s
487
+ * `dismissalCooldownMinutes` prop for the auto-show suppression policy.
488
+ */
489
+ declare const CONSENT_DISMISSAL_TIMESTAMP_KEY = "ffid_consent_dismissed_at";
481
490
  interface WriteConsentCookieOptions {
482
491
  /** Override Secure flag for non-https custom domains. Defaults to production. */
483
492
  secure?: boolean;
@@ -521,6 +530,34 @@ declare function deleteConsentCookie(opts?: WriteConsentCookieOptions): boolean;
521
530
  declare function readConsentSessionMirror(): FFIDConsentCategories | null;
522
531
  declare function writeConsentSessionMirror(cats: FFIDConsentCategories): boolean;
523
532
  declare function clearConsentSessionMirror(): void;
533
+ /**
534
+ * Read the millisecond unix timestamp at which the banner was last dismissed,
535
+ * or `null` if absent / invalid / sessionStorage unavailable.
536
+ *
537
+ * Defensive parsing rejects non-finite values, negatives, and future-dated
538
+ * timestamps (clock-skew / tampering). When a corrupted value is detected,
539
+ * the helper **auto-heals** by clearing the slot — otherwise the same bad
540
+ * value would be re-parsed on every Provider mount and a buggy / extension-
541
+ * polluted consumer site could leave the cooldown effectively permanently
542
+ * disabled with no surface signal.
543
+ */
544
+ declare function readConsentDismissalTimestamp(): number | null;
545
+ /**
546
+ * Write the dismissal timestamp. Returns `true` on success, `false` when
547
+ * sessionStorage is unavailable / throws (e.g., quota, old Safari Private
548
+ * Browsing) **or** when `nowMs` is non-finite / negative.
549
+ *
550
+ * Callers may safely ignore the return — failure degrades to the legacy
551
+ * "reprompt every Provider mount" behavior, which is the published v5.1.0
552
+ * baseline. Dismissal carries no GDPR weight (EDPB §86) so there is no
553
+ * obligation to surface storage failure via `onError`.
554
+ */
555
+ declare function writeConsentDismissalTimestamp(nowMs?: number): boolean;
556
+ /**
557
+ * Idempotent clear of the dismissal timestamp. Safe to call when no value is
558
+ * present or sessionStorage is unavailable; never throws.
559
+ */
560
+ declare function clearConsentDismissalTimestamp(): void;
524
561
 
525
562
  /**
526
563
  * Cookie Consent — Google Consent Mode v2 wiring.
@@ -653,11 +690,29 @@ interface FFIDAnalyticsProviderProps {
653
690
  onError?: (error: FFIDConsentError) => void;
654
691
  /** Whether to open banner automatically when no decision is on file. Default: true. */
655
692
  autoShowBanner?: boolean;
693
+ /**
694
+ * Minutes to suppress auto-reopen after the user presses "あとで" (closeBanner).
695
+ *
696
+ * The dismissal timestamp is held in `sessionStorage` only — never written
697
+ * to cookie or DB — so it expires when the tab closes. This is EDPB
698
+ * Guidelines 05/2020 §86 compliant: dismissal is not a refusal, and the
699
+ * user is reprompted on the next fresh visit regardless of cooldown.
700
+ *
701
+ * Accepts any `number`; non-finite (`NaN` / `±Infinity`) and negative
702
+ * values are treated as `0` (suppression disabled). `0` reverts to the
703
+ * legacy v5.1.0 "reprompt on every Provider mount" behavior. Default: 30.
704
+ *
705
+ * Manual `openBanner()` calls ignore the cooldown so consumer "Cookie 設定"
706
+ * links keep working, and `needsRenewal=true` overrides the cooldown so the
707
+ * renewal reprompt (CNIL/ICO renewal guidance — see PR #3261) is never
708
+ * suppressed by a UX dismissal.
709
+ */
710
+ dismissalCooldownMinutes?: number;
656
711
  /** Optional fetch override (mainly for tests). */
657
712
  fetchImpl?: typeof fetch;
658
713
  children: ReactNode;
659
714
  }
660
- declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
715
+ declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
661
716
 
662
717
  interface FFIDConsentContextValue {
663
718
  /** Current consent state. Always non-null; "not yet decided" is encoded by `hasDecided=false`. */
@@ -797,15 +852,23 @@ declare function useFFIDConsentPreferences(): UseFFIDConsentPreferencesReturn;
797
852
  * `<FFIDCookieBanner>` — bottom-sheet consent banner.
798
853
  *
799
854
  * Renders only when `state.hasDecided === false` or `state.needsRenewal === true`.
800
- * Three buttons (spec §6.2):
855
+ * Four buttons (spec §6.2):
801
856
  * 1. "すべて同意" (Accept all)
802
- * 2. "必要なものだけ" (Necessary only)
857
+ * 2. "同意しない" (Necessary only — `necessary=true` is enforced by ePrivacy
858
+ * 5(3) and the DB invariant; the 3 user-toggleable categories are
859
+ * recorded as false. The action itself is preserved in `source='banner'`)
803
860
  * 3. "詳細設定" (Open preference center)
861
+ * 4. "あとで" (Dismiss without recording a decision — banner re-appears on
862
+ * the next visit because no row is written)
804
863
  *
805
864
  * GDPR Article 7(1)/(2) UX requirements:
806
865
  * - "同意" and "拒否" share the same size + visual prominence.
807
866
  * - Buttons share the same styles by default; consumers can override via
808
- * `classNames` props (`primaryButton`, `secondaryButton`, `linkButton`).
867
+ * `classNames` props (`primaryButton`, `secondaryButton`, `linkButton`,
868
+ * `closeButton`).
869
+ * - "あとで" is intentionally not a refusal: EDPB Guidelines 05/2020 §86
870
+ * forbid treating dismissal as a refusal. We honor that by writing
871
+ * nothing on dismissal and re-prompting next visit.
809
872
  *
810
873
  * Refs: docs/02-design/COOKIE_CONSENT.md §6.2
811
874
  */
@@ -817,6 +880,7 @@ interface FFIDCookieBannerClassNames {
817
880
  primaryButton?: string;
818
881
  secondaryButton?: string;
819
882
  linkButton?: string;
883
+ closeButton?: string;
820
884
  }
821
885
  interface FFIDCookieBannerProps {
822
886
  /** Root container class. */
@@ -827,14 +891,40 @@ interface FFIDCookieBannerProps {
827
891
  message?: ReactNode;
828
892
  /** Accept-all button label. Default: "すべて同意". */
829
893
  acceptAllLabel?: ReactNode;
830
- /** Necessary-only button label. Default: "必要なものだけ". */
894
+ /**
895
+ * Necessary-only button label. Default: "同意しない".
896
+ *
897
+ * Selecting this button writes `necessary=true, functional=false,
898
+ * analytics=false, marketing=false` to the audit trail with
899
+ * `source='banner'`. The `necessary=true` value is enforced by the DB
900
+ * invariant; ePrivacy 5(3) exempts strictly necessary cookies from consent.
901
+ * Consumers that want to surface the legacy "必要なものだけ" wording can
902
+ * override via this prop.
903
+ */
831
904
  necessaryOnlyLabel?: ReactNode;
832
905
  /** Preference center link label. Default: "詳細設定". */
833
906
  preferencesLabel?: ReactNode;
907
+ /**
908
+ * Dismiss-without-recording button label. Default: "あとで".
909
+ *
910
+ * Selecting this button calls `closeBanner()` — session-only state, no row
911
+ * is written to the database. The banner re-appears on the next visit
912
+ * because bootstrap still sees `hasDecided=false`. Aligns with EDPB
913
+ * Guidelines 05/2020 §86 prohibiting treating dismissal as a refusal.
914
+ *
915
+ * NOTE for consumers: do NOT pass `closeLabel={null}` or `closeLabel=""`
916
+ * to visually hide the button while keeping the others. Doing so leaves
917
+ * a click-able empty area and breaks the GDPR Article 7(2) requirement
918
+ * that "consent" and "refusal" / "dismissal" controls share equal visual
919
+ * prominence. If you need to suppress dismissal entirely (e.g., for a
920
+ * strict-consent jurisdiction), build a custom banner using
921
+ * `useFFIDConsent()` instead of overriding the label.
922
+ */
923
+ closeLabel?: ReactNode;
834
924
  /** Custom root style. */
835
925
  style?: CSSProperties;
836
926
  }
837
- declare function FFIDCookieBanner({ className, classNames, message, acceptAllLabel, necessaryOnlyLabel, preferencesLabel, style, }: FFIDCookieBannerProps): React.JSX.Element | null;
927
+ declare function FFIDCookieBanner({ className, classNames, message, acceptAllLabel, necessaryOnlyLabel, preferencesLabel, closeLabel, style, }: FFIDCookieBannerProps): React.JSX.Element | null;
838
928
 
839
929
  /**
840
930
  * `<FFIDCookieSettings>` — preference center modal.
@@ -882,4 +972,4 @@ interface FFIDCookieLinkProps {
882
972
  }
883
973
  declare function FFIDCookieLink({ children, className, style, id, }: FFIDCookieLinkProps): React.JSX.Element;
884
974
 
885
- export { GetCategoriesWireSchema as $, ALL_DENIED_EXCEPT_NECESSARY as A, FFIDConsentMergeWarningSchema as B, CONSENT_COOKIE_MAX_AGE_SEC as C, DEFAULT_CONSENT_ERROR_MESSAGES as D, type FFIDConsentSource as E, type FFIDConsentResult as F, type GetConsentMeWire as G, FFIDConsentSourceSchema as H, FFIDConsentStateSchema as I, FFIDConsentUpdateSchema as J, FFIDCookieBanner as K, type FFIDCookieBannerClassNames as L, type FFIDCookieBannerProps as M, FFIDCookieLink as N, type FFIDCookieLinkProps as O, type PostConsentWire as P, FFIDCookiePolicySchema as Q, FFIDCookieSettings as R, type FFIDCookieSettingsClassNames as S, type FFIDCookieSettingsProps as T, type FFIDInternalConsentSource as U, FFIDInternalConsentSourceSchema as V, type FFIDPublicConsentSource as W, FFIDPublicConsentSourceSchema as X, FFID_CONSENT_ERROR_CODES as Y, FFID_CONSENT_NOT_DECIDED_STATE as Z, type GetCategoriesWire as _, type FFIDConsentState as a, GetConsentMeWireSchema as a0, type GetDocumentWire as a1, GetDocumentWireSchema as a2, type GtagBridge as a3, type GtagBridgeOptions as a4, type PostConsentRequest as a5, PostConsentRequestSchema as a6, PostConsentWireSchema as a7, type PostSyncRequest as a8, PostSyncRequestSchema as a9, PostSyncWireSchema as aa, PostWithdrawWireSchema as ab, type UseFFIDConsentPreferencesReturn as ac, type UseFFIDConsentReturn as ad, clearConsentSessionMirror as ae, createGtagBridge as af, decodeConsentCookie as ag, deleteConsentCookie as ah, encodeConsentCookie as ai, mapCategoriesToGtagParams as aj, readConsentCookie as ak, readConsentSessionMirror as al, useFFIDConsent as am, useFFIDConsentPreferences as an, writeConsentCookie as ao, writeConsentSessionMirror as ap, type FFIDConsentUpdate as b, type FFIDConsentCategories as c, type FFIDConsentSyncResult as d, type FFIDConsentCategoryMetadata as e, type FFIDCookiePolicy as f, type PostSyncWire as g, type PostWithdrawWire as h, CONSENT_COOKIE_NAME as i, CONSENT_SESSION_STORAGE_KEY as j, COOKIE_VERSION as k, DEFAULT_MERGE_WARNING_MESSAGES as l, DeviceIdSchema as m, FFIDAnalyticsProvider as n, type FFIDAnalyticsProviderProps as o, FFIDConsentCategoriesSchema as p, type FFIDConsentCategoryCode as q, FFIDConsentCategoryCodeSchema as r, FFIDConsentCategoryMetadataSchema as s, FFIDConsentContext as t, type FFIDConsentContextValue as u, FFIDConsentError as v, type FFIDConsentErrorCode as w, type FFIDConsentMergeStrategy as x, FFIDConsentMergeStrategySchema as y, type FFIDConsentMergeWarning as z };
975
+ export { type GetCategoriesWire as $, ALL_DENIED_EXCEPT_NECESSARY as A, type FFIDConsentMergeWarning as B, CONSENT_COOKIE_MAX_AGE_SEC as C, DEFAULT_CONSENT_ERROR_MESSAGES as D, FFIDConsentMergeWarningSchema as E, type FFIDConsentResult as F, type GetConsentMeWire as G, type FFIDConsentSource as H, FFIDConsentSourceSchema as I, FFIDConsentStateSchema as J, FFIDConsentUpdateSchema as K, FFIDCookieBanner as L, type FFIDCookieBannerClassNames as M, type FFIDCookieBannerProps as N, FFIDCookieLink as O, type PostConsentWire as P, type FFIDCookieLinkProps as Q, FFIDCookiePolicySchema as R, FFIDCookieSettings as S, type FFIDCookieSettingsClassNames as T, type FFIDCookieSettingsProps as U, type FFIDInternalConsentSource as V, FFIDInternalConsentSourceSchema as W, type FFIDPublicConsentSource as X, FFIDPublicConsentSourceSchema as Y, FFID_CONSENT_ERROR_CODES as Z, FFID_CONSENT_NOT_DECIDED_STATE as _, type FFIDConsentState as a, GetCategoriesWireSchema as a0, GetConsentMeWireSchema as a1, type GetDocumentWire as a2, GetDocumentWireSchema as a3, type GtagBridge as a4, type GtagBridgeOptions as a5, type PostConsentRequest as a6, PostConsentRequestSchema as a7, PostConsentWireSchema as a8, type PostSyncRequest as a9, PostSyncRequestSchema as aa, PostSyncWireSchema as ab, PostWithdrawWireSchema as ac, type UseFFIDConsentPreferencesReturn as ad, type UseFFIDConsentReturn as ae, clearConsentDismissalTimestamp as af, clearConsentSessionMirror as ag, createGtagBridge as ah, decodeConsentCookie as ai, deleteConsentCookie as aj, encodeConsentCookie as ak, mapCategoriesToGtagParams as al, readConsentCookie as am, readConsentDismissalTimestamp as an, readConsentSessionMirror as ao, useFFIDConsent as ap, useFFIDConsentPreferences as aq, writeConsentCookie as ar, writeConsentDismissalTimestamp as as, writeConsentSessionMirror as at, type FFIDConsentUpdate as b, type FFIDConsentCategories as c, type FFIDConsentSyncResult as d, type FFIDConsentCategoryMetadata as e, type FFIDCookiePolicy as f, type PostSyncWire as g, type PostWithdrawWire as h, CONSENT_COOKIE_NAME as i, CONSENT_DISMISSAL_TIMESTAMP_KEY as j, CONSENT_SESSION_STORAGE_KEY as k, COOKIE_VERSION as l, DEFAULT_MERGE_WARNING_MESSAGES as m, DeviceIdSchema as n, FFIDAnalyticsProvider as o, type FFIDAnalyticsProviderProps as p, FFIDConsentCategoriesSchema as q, type FFIDConsentCategoryCode as r, FFIDConsentCategoryCodeSchema as s, FFIDConsentCategoryMetadataSchema as t, FFIDConsentContext as u, type FFIDConsentContextValue as v, FFIDConsentError as w, type FFIDConsentErrorCode as x, type FFIDConsentMergeStrategy as y, FFIDConsentMergeStrategySchema as z };
@@ -840,7 +840,7 @@ function createProfileMethods(deps) {
840
840
  }
841
841
 
842
842
  // src/client/version-check.ts
843
- var SDK_VERSION = "5.0.2";
843
+ var SDK_VERSION = "5.2.0";
844
844
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
845
845
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
846
846
  function sdkHeaders() {
@@ -838,7 +838,7 @@ function createProfileMethods(deps) {
838
838
  }
839
839
 
840
840
  // src/client/version-check.ts
841
- var SDK_VERSION = "5.0.2";
841
+ var SDK_VERSION = "5.2.0";
842
842
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
843
843
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
844
844
  function sdkHeaders() {
@@ -284,6 +284,7 @@ var HOURS_PER_DAY = 24;
284
284
  var DAYS_PER_YEAR = 365;
285
285
  var CONSENT_COOKIE_MAX_AGE_SEC = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * DAYS_PER_YEAR;
286
286
  var CONSENT_SESSION_STORAGE_KEY = "ffid_consent_state";
287
+ var CONSENT_DISMISSAL_TIMESTAMP_KEY = "ffid_consent_dismissed_at";
287
288
  function hasDocument() {
288
289
  return typeof document !== "undefined";
289
290
  }
@@ -374,6 +375,39 @@ function clearConsentSessionMirror() {
374
375
  } catch {
375
376
  }
376
377
  }
378
+ function readConsentDismissalTimestamp() {
379
+ if (!hasSessionStorage()) return null;
380
+ try {
381
+ const raw = sessionStorage.getItem(CONSENT_DISMISSAL_TIMESTAMP_KEY);
382
+ if (!raw) return null;
383
+ const parsed = Number(raw);
384
+ const CLOCK_SKEW_GRACE_MS = 6e4;
385
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > Date.now() + CLOCK_SKEW_GRACE_MS) {
386
+ clearConsentDismissalTimestamp();
387
+ return null;
388
+ }
389
+ return parsed;
390
+ } catch {
391
+ return null;
392
+ }
393
+ }
394
+ function writeConsentDismissalTimestamp(nowMs = Date.now()) {
395
+ if (!Number.isFinite(nowMs) || nowMs < 0) return false;
396
+ if (!hasSessionStorage()) return false;
397
+ try {
398
+ sessionStorage.setItem(CONSENT_DISMISSAL_TIMESTAMP_KEY, String(nowMs));
399
+ return true;
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ function clearConsentDismissalTimestamp() {
405
+ if (!hasSessionStorage()) return;
406
+ try {
407
+ sessionStorage.removeItem(CONSENT_DISMISSAL_TIMESTAMP_KEY);
408
+ } catch {
409
+ }
410
+ }
377
411
 
378
412
  // src/consent/storage/device-id.ts
379
413
  var DEVICE_ID_LOCAL_STORAGE_KEY = "ffid_device_id";
@@ -1014,6 +1048,8 @@ var DEFAULT_MERGE_WARNING_MESSAGES = {
1014
1048
  shared_device_detected: "\u3053\u306E\u7AEF\u672B\u306F\u5225\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u3067\u3082\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059\u3002Cookie \u8A2D\u5B9A\u306F\u30A2\u30AB\u30A6\u30F3\u30C8\u3054\u3068\u306B\u4FDD\u5B58\u3055\u308C\u3066\u3044\u307E\u3059",
1015
1049
  drift_detected: "\u4FDD\u5B58\u3055\u308C\u3066\u3044\u305F\u8A2D\u5B9A\u3068\u7570\u306A\u308B\u9805\u76EE\u304C\u3042\u3063\u305F\u305F\u3081\u3001\u65B0\u3057\u304F\u8A18\u9332\u3055\u308C\u307E\u3057\u305F"
1016
1050
  };
1051
+ var DEFAULT_DISMISSAL_COOLDOWN_MINUTES = 30;
1052
+ var MS_PER_MINUTE = 6e4;
1017
1053
  function applyChanges(base, changes) {
1018
1054
  return {
1019
1055
  necessary: true,
@@ -1032,9 +1068,17 @@ function FFIDAnalyticsProvider({
1032
1068
  onConsentChange,
1033
1069
  onError,
1034
1070
  autoShowBanner = true,
1071
+ dismissalCooldownMinutes = DEFAULT_DISMISSAL_COOLDOWN_MINUTES,
1035
1072
  fetchImpl,
1036
1073
  children
1037
1074
  }) {
1075
+ const cooldownMs = Number.isFinite(dismissalCooldownMinutes) && dismissalCooldownMinutes > 0 ? dismissalCooldownMinutes * MS_PER_MINUTE : 0;
1076
+ const isWithinDismissalCooldown = react.useCallback(() => {
1077
+ if (cooldownMs <= 0) return false;
1078
+ const dismissedAt = readConsentDismissalTimestamp();
1079
+ if (dismissedAt === null) return false;
1080
+ return Date.now() - dismissedAt < cooldownMs;
1081
+ }, [cooldownMs]);
1038
1082
  const [state, setState] = react.useState(
1039
1083
  FFID_CONSENT_NOT_DECIDED_STATE
1040
1084
  );
@@ -1145,7 +1189,7 @@ function FFIDAnalyticsProvider({
1145
1189
  applyCategoriesSideEffects(result.value.categories);
1146
1190
  writeConsentSessionMirror(result.value.categories);
1147
1191
  setIsLoading(false);
1148
- if (!result.value.hasDecided && autoShowBanner) {
1192
+ if (!result.value.hasDecided && autoShowBanner && !isWithinDismissalCooldown()) {
1149
1193
  setIsBannerOpen(true);
1150
1194
  }
1151
1195
  if (result.value.needsRenewal && autoShowBanner) {
@@ -1273,7 +1317,10 @@ function FFIDAnalyticsProvider({
1273
1317
  persistCategories
1274
1318
  ]);
1275
1319
  const openBanner = react.useCallback(() => setIsBannerOpen(true), []);
1276
- const closeBanner = react.useCallback(() => setIsBannerOpen(false), []);
1320
+ const closeBanner = react.useCallback(() => {
1321
+ if (cooldownMs > 0) writeConsentDismissalTimestamp();
1322
+ setIsBannerOpen(false);
1323
+ }, [cooldownMs]);
1277
1324
  const openPreferences = react.useCallback(async () => {
1278
1325
  if (!categoryMetadata) {
1279
1326
  const client = clientRef.current;
@@ -1403,6 +1450,7 @@ function FFIDCookieBanner({
1403
1450
  acceptAllLabel,
1404
1451
  necessaryOnlyLabel,
1405
1452
  preferencesLabel,
1453
+ closeLabel,
1406
1454
  style
1407
1455
  }) {
1408
1456
  const {
@@ -1411,6 +1459,7 @@ function FFIDCookieBanner({
1411
1459
  acceptAll,
1412
1460
  acceptNecessaryOnly,
1413
1461
  openPreferences,
1462
+ closeBanner,
1414
1463
  error
1415
1464
  } = useFFIDConsent();
1416
1465
  if (!isBannerOpen && state.hasDecided && !state.needsRenewal) return null;
@@ -1462,7 +1511,7 @@ function FFIDCookieBanner({
1462
1511
  void acceptNecessaryOnly();
1463
1512
  },
1464
1513
  "data-testid": "ffid-cookie-banner-necessary-only",
1465
- children: necessaryOnlyLabel ?? "\u5FC5\u8981\u306A\u3082\u306E\u3060\u3051"
1514
+ children: necessaryOnlyLabel ?? "\u540C\u610F\u3057\u306A\u3044"
1466
1515
  }
1467
1516
  ),
1468
1517
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1477,6 +1526,19 @@ function FFIDCookieBanner({
1477
1526
  "data-testid": "ffid-cookie-banner-preferences",
1478
1527
  children: preferencesLabel ?? "\u8A73\u7D30\u8A2D\u5B9A"
1479
1528
  }
1529
+ ),
1530
+ /* @__PURE__ */ jsxRuntime.jsx(
1531
+ "button",
1532
+ {
1533
+ type: "button",
1534
+ className: classNames?.closeButton,
1535
+ style: DEFAULT_BUTTON_STYLE,
1536
+ onClick: () => {
1537
+ closeBanner();
1538
+ },
1539
+ "data-testid": "ffid-cookie-banner-close",
1540
+ children: closeLabel ?? "\u3042\u3068\u3067"
1541
+ }
1480
1542
  )
1481
1543
  ]
1482
1544
  }
@@ -1780,6 +1842,7 @@ function FFIDCookieLink({
1780
1842
  exports.ALL_DENIED_EXCEPT_NECESSARY = ALL_DENIED_EXCEPT_NECESSARY;
1781
1843
  exports.CONSENT_COOKIE_MAX_AGE_SEC = CONSENT_COOKIE_MAX_AGE_SEC;
1782
1844
  exports.CONSENT_COOKIE_NAME = CONSENT_COOKIE_NAME;
1845
+ exports.CONSENT_DISMISSAL_TIMESTAMP_KEY = CONSENT_DISMISSAL_TIMESTAMP_KEY;
1783
1846
  exports.CONSENT_SESSION_STORAGE_KEY = CONSENT_SESSION_STORAGE_KEY;
1784
1847
  exports.COOKIE_VERSION = COOKIE_VERSION;
1785
1848
  exports.DEFAULT_CONSENT_ERROR_MESSAGES = DEFAULT_CONSENT_ERROR_MESSAGES;
@@ -1814,6 +1877,7 @@ exports.PostConsentWireSchema = PostConsentWireSchema;
1814
1877
  exports.PostSyncRequestSchema = PostSyncRequestSchema;
1815
1878
  exports.PostSyncWireSchema = PostSyncWireSchema;
1816
1879
  exports.PostWithdrawWireSchema = PostWithdrawWireSchema;
1880
+ exports.clearConsentDismissalTimestamp = clearConsentDismissalTimestamp;
1817
1881
  exports.clearConsentSessionMirror = clearConsentSessionMirror;
1818
1882
  exports.clearDeviceId = clearDeviceId;
1819
1883
  exports.createConsentClient = createConsentClient;
@@ -1831,8 +1895,10 @@ exports.mapMeWireToState = mapMeWireToState;
1831
1895
  exports.mapSyncWireToResult = mapSyncWireToResult;
1832
1896
  exports.mapWithdrawWireToState = mapWithdrawWireToState;
1833
1897
  exports.readConsentCookie = readConsentCookie;
1898
+ exports.readConsentDismissalTimestamp = readConsentDismissalTimestamp;
1834
1899
  exports.readConsentSessionMirror = readConsentSessionMirror;
1835
1900
  exports.useFFIDConsent = useFFIDConsent;
1836
1901
  exports.useFFIDConsentPreferences = useFFIDConsentPreferences;
1837
1902
  exports.writeConsentCookie = writeConsentCookie;
1903
+ exports.writeConsentDismissalTimestamp = writeConsentDismissalTimestamp;
1838
1904
  exports.writeConsentSessionMirror = writeConsentSessionMirror;