@vtex/faststore 0.0.4 → 0.0.6
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.
- package/CHANGELOG.md +67 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1595 -481
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +31 -7
- package/dist/components.js +35 -13
- package/dist/content-types.json +73 -0
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/account-access-gate.md +153 -0
- package/dist/docs/account-entry-point.md +1 -2
- package/dist/docs/account-menu.md +164 -0
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/buy-button.md +1 -1
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/cart.md +1 -1
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +1 -0
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/session.md +1 -1
- package/dist/docs/shipping-address.md +110 -258
- package/dist/docs/sign-in-redirect.md +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +3 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/account-access-gate/account-access-gate.d.ts +86 -0
- package/dist/matter/account-access-gate/account-access-gate.js +283 -0
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -0
- package/dist/matter/account-access-gate/sign-out-intent.d.ts +17 -0
- package/dist/matter/account-access-gate/sign-out-intent.js +62 -0
- package/dist/matter/account-access-gate/sign-out-intent.js.map +1 -0
- package/dist/matter/account-entry-point/account-entry-point.js +2 -2
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +88 -0
- package/dist/matter/account-menu/account-menu.js +246 -0
- package/dist/matter/account-menu/account-menu.js.map +1 -0
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +832 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/cart/cart.d.ts +1 -1
- package/dist/matter/cart/cart.js +2 -2
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/delivery-options/delivery-options.js.map +1 -1
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.js +5 -5
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +107 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +263 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
- package/dist/matter/product-shelf/product-shelf.js +2 -0
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +387 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +481 -792
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/footer-social/footer-social.js +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/incentives/incentives.js +1 -1
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +701 -2
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/cart-action-provider.d.ts +1 -1
- package/dist/sdk/actions/cart-action-provider.js +1 -1
- package/dist/sdk/actions/cart-action-provider.js.map +1 -1
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/session-action-provider.d.ts +1 -1
- package/dist/sdk/actions/session-action-provider.js +1 -1
- package/dist/sdk/actions/session-action-provider.js.map +1 -1
- package/dist/sdk/actions/shipping-action-provider.d.ts +1 -1
- package/dist/sdk/actions/shipping-action-provider.js +1 -1
- package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +108 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cache/storefront-cache.js +1 -1
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/cart/operations.d.ts +4 -4
- package/dist/sdk/cart/operations.js +1 -1
- package/dist/sdk/cart/operations.js.map +1 -1
- package/dist/sdk/cart/optimistic.js +1 -1
- package/dist/sdk/cart/optimistic.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.d.ts +1 -1
- package/dist/sdk/cart/use-cart-actions.js +4 -4
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
- package/dist/sdk/cart/write-gate.js +1 -1
- package/dist/sdk/cart/write-gate.js.map +1 -1
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/index.js +1 -0
- package/dist/sdk/data/page-reactive-data-root.d.ts +38 -2
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.d.ts +1 -1
- package/dist/sdk/data/reactive-data-provider.js +8 -4
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.d.ts +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +13 -5
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +6 -2
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/gql.js +2 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.js +85 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/operations.js.map +1 -1
- package/dist/sdk/identity/use-identity-actions.d.ts +1 -1
- package/dist/sdk/identity/use-identity-actions.js +3 -2
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/session/auth-operations.d.ts +1 -1
- package/dist/sdk/session/auth-operations.js.map +1 -1
- package/dist/sdk/session/use-session-actions.d.ts +1 -1
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/session/use-session-revalidation.js +65 -0
- package/dist/sdk/session/use-session-revalidation.js.map +1 -0
- package/dist/sdk/shipping/option-select-operations.d.ts +1 -1
- package/dist/sdk/shipping/option-select-operations.js.map +1 -1
- package/dist/sdk/shipping/request-timeout.js +1 -1
- package/dist/sdk/shipping/request-timeout.js.map +1 -1
- package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
- package/dist/sdk/shipping/shipping-operations.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.d.ts +2 -2
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/sdk/state/cart.d.ts +3 -4
- package/dist/sdk/state/cart.js +6 -7
- package/dist/sdk/state/cart.js.map +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/safe-href.js +3 -3
- package/dist/utils/safe-href.js.map +1 -1
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +2 -1
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -5,6 +5,7 @@ import { AccessCodeField } from "../../particles/access-code-field/access-code-f
|
|
|
5
5
|
import { EmailField, isValidEmail } from "../../particles/email-field/email-field.js";
|
|
6
6
|
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
7
7
|
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
8
|
+
import { resolveAuthoredCopy } from "../../utils/resolve-authored-copy.js";
|
|
8
9
|
import { useSession } from "../session/session.js";
|
|
9
10
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -35,18 +36,18 @@ function useAccessCodeSignIn() {
|
|
|
35
36
|
}
|
|
36
37
|
function requestErrorMessage(error, props) {
|
|
37
38
|
switch (error.code) {
|
|
38
|
-
case "ACCESS_CODE_SEND_LIMIT": return props.sendLimitMessage
|
|
39
|
-
case "BLOCKED": return props.blockedMessage
|
|
40
|
-
case "NETWORK_ERROR": return props.networkErrorMessage
|
|
39
|
+
case "ACCESS_CODE_SEND_LIMIT": return resolveAuthoredCopy(props.sendLimitMessage, error.message);
|
|
40
|
+
case "BLOCKED": return resolveAuthoredCopy(props.blockedMessage, error.message);
|
|
41
|
+
case "NETWORK_ERROR": return resolveAuthoredCopy(props.networkErrorMessage, error.message);
|
|
41
42
|
default: return error.message;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
function validateErrorMessage(error, props) {
|
|
45
46
|
switch (error.code) {
|
|
46
|
-
case "INVALID_ACCESS_CODE": return props.invalidCodeMessage
|
|
47
|
-
case "BLOCKED": return props.blockedMessage
|
|
48
|
-
case "MISSING_AUTH_CONTEXT": return props.missingAuthContextMessage
|
|
49
|
-
case "NETWORK_ERROR": return props.networkErrorMessage
|
|
47
|
+
case "INVALID_ACCESS_CODE": return resolveAuthoredCopy(props.invalidCodeMessage, error.message);
|
|
48
|
+
case "BLOCKED": return resolveAuthoredCopy(props.blockedMessage, error.message);
|
|
49
|
+
case "MISSING_AUTH_CONTEXT": return resolveAuthoredCopy(props.missingAuthContextMessage, error.message);
|
|
50
|
+
case "NETWORK_ERROR": return resolveAuthoredCopy(props.networkErrorMessage, error.message);
|
|
50
51
|
default: return error.message;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -295,7 +296,7 @@ function AccessCodeRequestControl({ label, pendingLabel, sendLimitMessage, block
|
|
|
295
296
|
"aria-busy": pending || void 0,
|
|
296
297
|
"data-fs-access-code-request-submit": "",
|
|
297
298
|
"data-fs-button": "",
|
|
298
|
-
children: pending ? pendingLabel ??
|
|
299
|
+
children: pending ? resolveAuthoredCopy(pendingLabel, label ?? "") : label
|
|
299
300
|
}), message ? /* @__PURE__ */ jsx("div", {
|
|
300
301
|
role: "alert",
|
|
301
302
|
"data-fs-access-code-request-error": "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-code-sign-in.js","names":[],"sources":["../../../src/matter/access-code-sign-in/access-code-sign-in.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef, FormEvent } from 'react';\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { AccessCodeField } from '../../particles/access-code-field/access-code-field.js';\nimport { EmailField, isValidEmail } from '../../particles/email-field/email-field.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport type { SessionUserError } from '../../sdk/session/auth-operations.js';\nimport { useSession } from '../session/session.js';\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment (colocated, ADR-0011) — clientProfile.email seeds the draft email as a\n// convenience pre-fill for an already-identified shopper; sessionAuthProviders.accessKeyAuthentication\n// is the account gate. Authored as a raw string, mirroring EmailIdentificationControl/ShippingSimulator.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment AccessCodeSignInData on Query {\n clientProfile {\n __typename\n id\n email\n }\n sessionAuthProviders {\n accessKeyAuthentication\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Colocated fragment slice forwarded by the reactive runtime — `null` until both fields resolve. */\nexport type AccessCodeSignInSlice = {\n clientProfile: { email: string | null } | null;\n sessionAuthProviders: { accessKeyAuthentication: boolean } | null;\n};\n\nexport type AccessCodeSignInStep = 'request' | 'validate';\n\nexport type AccessCodeSignInContextValue = Readonly<{\n /** The shopper's locally-typed draft email — never written to the cart; only ever an argument to\n * requestAccessCode/validateAccessCode. Pre-fills from `clientProfile.email` when known, but a\n * shopper edit is never persisted anywhere by this family. */\n email: string;\n /** Updates the draft email; marks it as shopper-touched so pre-fill no longer overwrites it. */\n setEmail: (email: string) => void;\n step: AccessCodeSignInStep;\n /** Opaque token round-tripped from requestAccessCode into validateAccessCode. */\n authenticationToken: string | null;\n /** Called by AccessCodeRequestControl on a successful requestAccessCode. */\n startValidation: (authenticationToken: string) => void;\n}>;\n\nexport type AccessCodeSignInRootProps = ComponentPropsWithRef<'div'> & {\n $data?: AccessCodeSignInSlice | null;\n};\n\nexport type AccessCodeEmailControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n placeholder?: string;\n invalidEmailMessage?: string;\n};\n\nexport type AccessCodeRequestControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n pendingLabel?: string;\n sendLimitMessage?: string;\n blockedMessage?: string;\n networkErrorMessage?: string;\n onRequested?: (authenticationToken: string) => void;\n $actions?: ComponentActions;\n};\n\nexport type AccessCodeValidateControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n placeholder?: string;\n submitLabel?: string;\n codeLength?: number;\n invalidCodeMessage?: string;\n blockedMessage?: string;\n missingAuthContextMessage?: string;\n networkErrorMessage?: string;\n onValidated?: () => void;\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst ACCESS_CODE_SIGN_IN_CONTEXT_UNSET = Symbol('AccessCodeSignInContextUnset');\n\nconst AccessCodeSignInContext = createContext<AccessCodeSignInContextValue | typeof ACCESS_CODE_SIGN_IN_CONTEXT_UNSET>(\n ACCESS_CODE_SIGN_IN_CONTEXT_UNSET,\n);\n\n/**\n * Read the stepper state (email, step, authenticationToken) from the nearest `AccessCodeSignInRoot`.\n * Throws when called outside that subtree — a misplaced control is a composition bug that should\n * surface immediately, mirroring `useShippingSimulator()`/`useSession()`.\n */\nfunction useAccessCodeSignIn(): AccessCodeSignInContextValue {\n const ctx = useContext(AccessCodeSignInContext);\n if (ctx === ACCESS_CODE_SIGN_IN_CONTEXT_UNSET) {\n throw new Error('AccessCodeSignIn components must be used inside AccessCodeSignInRoot.');\n }\n return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// Error-code → message mapping — every failure surfaces inline, at the control that produced it;\n// an unmapped code falls back to the server-supplied message rather than being silent.\n// ---------------------------------------------------------------------------\n\nfunction requestErrorMessage(\n error: SessionUserError,\n props: Pick<AccessCodeRequestControlProps, 'sendLimitMessage' | 'blockedMessage' | 'networkErrorMessage'>,\n): string {\n switch (error.code) {\n case 'ACCESS_CODE_SEND_LIMIT':\n return props.sendLimitMessage ?? error.message;\n case 'BLOCKED':\n return props.blockedMessage ?? error.message;\n case 'NETWORK_ERROR':\n return props.networkErrorMessage ?? error.message;\n default:\n return error.message;\n }\n}\n\nfunction validateErrorMessage(\n error: SessionUserError,\n props: Pick<\n AccessCodeValidateControlProps,\n 'invalidCodeMessage' | 'blockedMessage' | 'missingAuthContextMessage' | 'networkErrorMessage'\n >,\n): string {\n switch (error.code) {\n case 'INVALID_ACCESS_CODE':\n return props.invalidCodeMessage ?? error.message;\n case 'BLOCKED':\n return props.blockedMessage ?? error.message;\n case 'MISSING_AUTH_CONTEXT':\n return props.missingAuthContextMessage ?? error.message;\n case 'NETWORK_ERROR':\n return props.networkErrorMessage ?? error.message;\n default:\n return error.message;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const AccessCodeSignInRootDefinition = defineComponent<AccessCodeSignInRootProps>({\n name: 'AccessCodeSignInRoot',\n importPath: '@vtex/faststore/components',\n component: AccessCodeSignInRoot,\n fragment,\n fetchStrategy: 'reactive',\n requiresContext: 'session',\n editor: { label: 'Access Code Sign-In', category: 'identity' },\n schema: {},\n guideline:\n 'Self-gating root of the anonymous-or-identified, email + access-code sign-in: renders nothing once loggedIn or when the account disables accessKeyAuthentication, but is available from the anonymous state onward. Owns a locally-typed draft email (never written to the cart) and the request/validate stepper. Must be inside SessionRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeEmailControlDefinition = defineComponent<AccessCodeEmailControlProps>({\n name: 'AccessCodeEmailControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeEmailControl,\n editor: { label: 'Access Code Email', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n invalidEmailMessage: { type: 'string', label: 'Invalid email message' },\n },\n guideline:\n 'The draft email input for the access-code flow: visible only at the request step, pre-fills from clientProfile.email when known but never persists an edit anywhere — it is purely the argument requestAccessCode/validateAccessCode dispatch with. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeRequestControlDefinition = defineComponent<AccessCodeRequestControlProps>({\n name: 'AccessCodeRequestControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeRequestControl,\n actions: { session: ['requestAccessCode'] },\n editor: { label: 'Access Code Request', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n sendLimitMessage: { type: 'string', label: 'Send-limit message' },\n blockedMessage: { type: 'string', label: 'Blocked message' },\n networkErrorMessage: { type: 'string', label: 'Network error message' },\n },\n guideline:\n 'Send-code step: disabled until the Root-owned draft email is a valid, non-empty address; dispatches $actions.session.requestAccessCode with it and advances the stepper on success. Failures render inline, never as a blocking error. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeValidateControlDefinition = defineComponent<AccessCodeValidateControlProps>({\n name: 'AccessCodeValidateControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeValidateControl,\n actions: { session: ['validateAccessCode'] },\n editor: { label: 'Access Code Validate', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n submitLabel: { type: 'string', label: 'Submit label' },\n codeLength: { type: 'number', default: 6, label: 'Code length' },\n invalidCodeMessage: { type: 'string', label: 'Invalid code message' },\n blockedMessage: { type: 'string', label: 'Blocked message' },\n networkErrorMessage: { type: 'string', label: 'Network error message' },\n },\n guideline:\n 'Code-entry step: composes AccessCodeField and dispatches $actions.session.validateAccessCode with the Root-owned email and authenticationToken; a successful validate flips the session reactively and hides the Root. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Data-owning root of the access-code sign-in flow. Self-gates on two signals:\n * `useSession().state !== 'loggedIn'` (available from anonymous through identified; hides once\n * signed in) and `sessionAuthProviders.accessKeyAuthentication === true`. An unresolved fragment\n * read (`$data` still `null`) is treated as \"provider not enabled\" — the flow stays hidden rather\n * than flashing. Owns the `step`/`authenticationToken` stepper plus a locally-typed draft `email`\n * as local state, broadcasting all of it through context to its children. The draft email pre-fills\n * from `clientProfile.email` once (for an already-identified shopper) but a shopper edit is never\n * persisted anywhere by this family — it is purely the requestAccessCode/validateAccessCode\n * argument; the server sets clientProfileData.email as a side effect of a successful validate.\n * Reacts automatically once `useSession()` flips to `loggedIn` — no manual unmount call anywhere in\n * this family.\n */\nexport function AccessCodeSignInRoot({ $data: $dataProp, className, children, ...props }: AccessCodeSignInRootProps) {\n const $data = useDataOverride($dataProp);\n const { state } = useSession();\n\n const [step, setStep] = useState<AccessCodeSignInStep>('request');\n const [authenticationToken, setAuthenticationToken] = useState<string | null>(null);\n\n const startValidation = useCallback((token: string) => {\n setAuthenticationToken(token);\n setStep('validate');\n }, []);\n\n const savedEmail = $data?.clientProfile?.email ?? '';\n const [email, setEmailState] = useState('');\n const [touched, setTouched] = useState(false);\n\n // Pre-fill from the resolving/known profile email, but only while still at the request step and\n // until the shopper edits the draft — mirrors ClientProfileForm's untouched-field re-seed, never\n // clobbering an in-progress edit. Freezing the re-seed once the flow leaves 'request' keeps a\n // mid-flow profile-cache change from diverging the email between requestAccessCode and\n // validateAccessCode (the server rejects a validate whose email differs from the request).\n useEffect(() => {\n if (step === 'request' && !touched) setEmailState(savedEmail);\n }, [savedEmail, touched, step]);\n\n const setEmail = useCallback((next: string) => {\n setTouched(true);\n setEmailState(next);\n }, []);\n\n const providerEnabled = $data?.sessionAuthProviders?.accessKeyAuthentication ?? false;\n\n const contextValue = useMemo<AccessCodeSignInContextValue>(\n () => ({ email, setEmail, step, authenticationToken, startValidation }),\n [email, setEmail, step, authenticationToken, startValidation],\n );\n\n const gated = state !== 'loggedIn' && providerEnabled;\n if (!gated) return null;\n\n return (\n <AccessCodeSignInContext value={contextValue}>\n <div {...props} className={className} data-fs-access-code-sign-in-root=\"\">\n {children}\n </div>\n </AccessCodeSignInContext>\n );\n}\n\n/**\n * The draft email input for the access-code flow. Renders only at the request step. Bound to the\n * Root's local `email`/`setEmail` — never a fragment, never a mutation of its own; typing here\n * never touches the cart. Format validation is `EmailField`'s own blur-triggered check (never\n * per-keystroke); `invalidEmailMessage` is only its fallback text, mirroring how\n * `EmailIdentificationControl` defers to the same particle for the same reason.\n */\nexport function AccessCodeEmailControl({\n label,\n placeholder,\n invalidEmailMessage,\n className,\n ...props\n}: AccessCodeEmailControlProps) {\n const { step, email, setEmail } = useAccessCodeSignIn();\n\n if (step !== 'request') return null;\n\n return (\n <div {...props} className={className} data-fs-access-code-email-control=\"\">\n <EmailField\n label={label}\n placeholder={placeholder}\n value={email}\n onValueChange={setEmail}\n errorMessage={invalidEmailMessage}\n />\n </div>\n );\n}\n\n/**\n * Renders only while the Root's stepper is at `step === 'request'`. Reads the draft email from\n * context and dispatches `$actions.session.requestAccessCode({ email })` once it is a valid,\n * non-empty address (no account-existence pre-check beyond that format gate). On a result with a\n * non-null token and no `userErrors`, advances the Root's stepper. Every `userErrors` entry renders\n * inline via a host-supplied, per-code message; the step never changes on error, so the shopper can\n * retry or abandon the method and continue as a guest.\n */\nexport function AccessCodeRequestControl({\n label,\n pendingLabel,\n sendLimitMessage,\n blockedMessage,\n networkErrorMessage,\n onRequested,\n $actions: $actionsProp,\n className,\n ...props\n}: AccessCodeRequestControlProps) {\n const $actions = useActionsOverride($actionsProp);\n const { step, email, startValidation } = useAccessCodeSignIn();\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<SessionUserError | null>(null);\n\n if (step !== 'request') return null;\n\n const canSubmit = !pending && isValidEmail(email);\n\n async function handleClick() {\n if (!canSubmit) return;\n setPending(true);\n setError(null);\n\n try {\n const result = (await $actions?.session?.requestAccessCode?.({ email })) ?? {\n authenticationToken: null,\n userErrors: [],\n };\n\n if (result.authenticationToken && result.userErrors.length === 0) {\n startValidation(result.authenticationToken);\n onRequested?.(result.authenticationToken);\n return;\n }\n\n const [firstError] = result.userErrors;\n if (firstError) setError(firstError);\n } finally {\n setPending(false);\n }\n }\n\n const message = error ? requestErrorMessage(error, { sendLimitMessage, blockedMessage, networkErrorMessage }) : null;\n\n return (\n <div\n {...props}\n className={className}\n data-fs-access-code-request-control=\"\"\n {...(pending ? { 'data-fs-access-code-request-pending': '' } : {})}\n >\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={!canSubmit}\n aria-busy={pending || undefined}\n data-fs-access-code-request-submit=\"\"\n data-fs-button=\"\"\n >\n {pending ? (pendingLabel ?? label) : label}\n </button>\n {message ? (\n <div role=\"alert\" data-fs-access-code-request-error=\"\">\n {message}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Renders only while the Root's stepper is at `step === 'validate'`. Composes `AccessCodeField` for\n * the code value and a submit control, disabled below `codeLength` (a UX courtesy, never a\n * validity claim). Dispatches `$actions.session.validateAccessCode` with the email and\n * `authenticationToken` read from context. On a non-empty `userErrors` result, sets the field's\n * `invalid`/`errorMessage` from a host-supplied per-code message and retains the code/token so the\n * shopper can retry without re-requesting. On success it calls the optional `onValidated` and renders\n * no local \"success\" UI — `useSession()` flipping to `loggedIn` is what makes the whole Root\n * disappear.\n */\nexport function AccessCodeValidateControl({\n label,\n placeholder,\n submitLabel,\n codeLength = 6,\n invalidCodeMessage,\n blockedMessage,\n missingAuthContextMessage,\n networkErrorMessage,\n onValidated,\n $actions: $actionsProp,\n className,\n ...props\n}: AccessCodeValidateControlProps) {\n const $actions = useActionsOverride($actionsProp);\n const { step, email, authenticationToken } = useAccessCodeSignIn();\n const [code, setCode] = useState('');\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<SessionUserError | null>(null);\n\n if (step !== 'validate') return null;\n\n const canSubmit = !pending && code.length >= codeLength && authenticationToken != null;\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!canSubmit || authenticationToken == null) return;\n\n setPending(true);\n setError(null);\n\n try {\n const errors =\n (await $actions?.session?.validateAccessCode?.({ email, accessCode: code, authenticationToken })) ?? [];\n\n const [firstError] = errors;\n if (firstError) {\n setError(firstError);\n return;\n }\n } finally {\n setPending(false);\n }\n\n onValidated?.();\n }\n\n function handleCodeChange(next: string) {\n setCode(next);\n if (error) setError(null);\n }\n\n const errorMessage = error\n ? validateErrorMessage(error, {\n invalidCodeMessage,\n blockedMessage,\n missingAuthContextMessage,\n networkErrorMessage,\n })\n : undefined;\n\n return (\n <div\n {...props}\n className={className}\n data-fs-access-code-validate-control=\"\"\n {...(pending ? { 'data-fs-access-code-validate-pending': '' } : {})}\n >\n <form onSubmit={handleSubmit}>\n <AccessCodeField\n label={label}\n placeholder={placeholder}\n value={code}\n maxLength={codeLength}\n onValueChange={handleCodeChange}\n disabled={pending}\n invalid={error != null}\n errorMessage={errorMessage}\n />\n <button\n type=\"submit\"\n disabled={!canSubmit}\n aria-busy={pending || undefined}\n data-fs-access-code-validate-submit=\"\"\n data-fs-button=\"\"\n >\n {submitLabel}\n </button>\n </form>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const AccessCodeSignIn = {\n Root: AccessCodeSignInRoot,\n Email: AccessCodeEmailControl,\n Request: AccessCodeRequestControl,\n Validate: AccessCodeValidateControl,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"access-code-sign-in.js","names":[],"sources":["../../../src/matter/access-code-sign-in/access-code-sign-in.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef, FormEvent } from 'react';\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { AccessCodeField } from '../../particles/access-code-field/access-code-field.js';\nimport { EmailField, isValidEmail } from '../../particles/email-field/email-field.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport type { SessionUserError } from '../../sdk/session/auth-operations.js';\nimport { resolveAuthoredCopy } from '../../utils/resolve-authored-copy.js';\nimport { useSession } from '../session/session.js';\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment (colocated, ADR-0011) — clientProfile.email seeds the draft email as a\n// convenience pre-fill for an already-identified shopper; sessionAuthProviders.accessKeyAuthentication\n// is the account gate. Authored as a raw string, mirroring EmailIdentificationControl/ShippingSimulator.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment AccessCodeSignInData on Query {\n clientProfile {\n __typename\n id\n email\n }\n sessionAuthProviders {\n accessKeyAuthentication\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Colocated fragment slice forwarded by the reactive runtime — `null` until both fields resolve. */\nexport type AccessCodeSignInSlice = {\n clientProfile: { email: string | null } | null;\n sessionAuthProviders: { accessKeyAuthentication: boolean } | null;\n};\n\nexport type AccessCodeSignInStep = 'request' | 'validate';\n\nexport type AccessCodeSignInContextValue = Readonly<{\n /** The shopper's locally-typed draft email — never written to the cart; only ever an argument to\n * requestAccessCode/validateAccessCode. Pre-fills from `clientProfile.email` when known, but a\n * shopper edit is never persisted anywhere by this family. */\n email: string;\n /** Updates the draft email; marks it as shopper-touched so pre-fill no longer overwrites it. */\n setEmail: (email: string) => void;\n step: AccessCodeSignInStep;\n /** Opaque token round-tripped from requestAccessCode into validateAccessCode. */\n authenticationToken: string | null;\n /** Called by AccessCodeRequestControl on a successful requestAccessCode. */\n startValidation: (authenticationToken: string) => void;\n}>;\n\nexport type AccessCodeSignInRootProps = ComponentPropsWithRef<'div'> & {\n $data?: AccessCodeSignInSlice | null;\n};\n\nexport type AccessCodeEmailControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n placeholder?: string;\n invalidEmailMessage?: string;\n};\n\nexport type AccessCodeRequestControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n pendingLabel?: string;\n sendLimitMessage?: string;\n blockedMessage?: string;\n networkErrorMessage?: string;\n onRequested?: (authenticationToken: string) => void;\n $actions?: ComponentActions;\n};\n\nexport type AccessCodeValidateControlProps = ComponentPropsWithRef<'div'> & {\n label?: string;\n placeholder?: string;\n submitLabel?: string;\n codeLength?: number;\n invalidCodeMessage?: string;\n blockedMessage?: string;\n missingAuthContextMessage?: string;\n networkErrorMessage?: string;\n onValidated?: () => void;\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst ACCESS_CODE_SIGN_IN_CONTEXT_UNSET = Symbol('AccessCodeSignInContextUnset');\n\nconst AccessCodeSignInContext = createContext<AccessCodeSignInContextValue | typeof ACCESS_CODE_SIGN_IN_CONTEXT_UNSET>(\n ACCESS_CODE_SIGN_IN_CONTEXT_UNSET,\n);\n\n/**\n * Read the stepper state (email, step, authenticationToken) from the nearest `AccessCodeSignInRoot`.\n * Throws when called outside that subtree — a misplaced control is a composition bug that should\n * surface immediately, mirroring `useShippingSimulator()`/`useSession()`.\n */\nfunction useAccessCodeSignIn(): AccessCodeSignInContextValue {\n const ctx = useContext(AccessCodeSignInContext);\n if (ctx === ACCESS_CODE_SIGN_IN_CONTEXT_UNSET) {\n throw new Error('AccessCodeSignIn components must be used inside AccessCodeSignInRoot.');\n }\n return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// Error-code → message mapping — every failure surfaces inline, at the control that produced it;\n// an unmapped code falls back to the server-supplied message rather than being silent. Each mapped\n// case is resolved through `resolveAuthoredCopy` (T38, FR-5): an authored empty or whitespace-only\n// message falls back to the server-supplied `error.message` rather than rendering a blank alert.\n// ---------------------------------------------------------------------------\n\nfunction requestErrorMessage(\n error: SessionUserError,\n props: Pick<AccessCodeRequestControlProps, 'sendLimitMessage' | 'blockedMessage' | 'networkErrorMessage'>,\n): string {\n switch (error.code) {\n case 'ACCESS_CODE_SEND_LIMIT':\n return resolveAuthoredCopy(props.sendLimitMessage, error.message);\n case 'BLOCKED':\n return resolveAuthoredCopy(props.blockedMessage, error.message);\n case 'NETWORK_ERROR':\n return resolveAuthoredCopy(props.networkErrorMessage, error.message);\n default:\n return error.message;\n }\n}\n\nfunction validateErrorMessage(\n error: SessionUserError,\n props: Pick<\n AccessCodeValidateControlProps,\n 'invalidCodeMessage' | 'blockedMessage' | 'missingAuthContextMessage' | 'networkErrorMessage'\n >,\n): string {\n switch (error.code) {\n case 'INVALID_ACCESS_CODE':\n return resolveAuthoredCopy(props.invalidCodeMessage, error.message);\n case 'BLOCKED':\n return resolveAuthoredCopy(props.blockedMessage, error.message);\n case 'MISSING_AUTH_CONTEXT':\n return resolveAuthoredCopy(props.missingAuthContextMessage, error.message);\n case 'NETWORK_ERROR':\n return resolveAuthoredCopy(props.networkErrorMessage, error.message);\n default:\n return error.message;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const AccessCodeSignInRootDefinition = defineComponent<AccessCodeSignInRootProps>({\n name: 'AccessCodeSignInRoot',\n importPath: '@vtex/faststore/components',\n component: AccessCodeSignInRoot,\n fragment,\n fetchStrategy: 'reactive',\n requiresContext: 'session',\n editor: { label: 'Access Code Sign-In', category: 'identity' },\n schema: {},\n guideline:\n 'Self-gating root of the anonymous-or-identified, email + access-code sign-in: renders nothing once loggedIn or when the account disables accessKeyAuthentication, but is available from the anonymous state onward. Owns a locally-typed draft email (never written to the cart) and the request/validate stepper. Must be inside SessionRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeEmailControlDefinition = defineComponent<AccessCodeEmailControlProps>({\n name: 'AccessCodeEmailControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeEmailControl,\n editor: { label: 'Access Code Email', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n invalidEmailMessage: { type: 'string', label: 'Invalid email message' },\n },\n guideline:\n 'The draft email input for the access-code flow: visible only at the request step, pre-fills from clientProfile.email when known but never persists an edit anywhere — it is purely the argument requestAccessCode/validateAccessCode dispatch with. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeRequestControlDefinition = defineComponent<AccessCodeRequestControlProps>({\n name: 'AccessCodeRequestControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeRequestControl,\n actions: { session: ['requestAccessCode'] },\n editor: { label: 'Access Code Request', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n sendLimitMessage: { type: 'string', label: 'Send-limit message' },\n blockedMessage: { type: 'string', label: 'Blocked message' },\n networkErrorMessage: { type: 'string', label: 'Network error message' },\n },\n guideline:\n 'Send-code step: disabled until the Root-owned draft email is a valid, non-empty address; dispatches $actions.session.requestAccessCode with it and advances the stepper on success. Failures render inline, never as a blocking error. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\nexport const AccessCodeValidateControlDefinition = defineComponent<AccessCodeValidateControlProps>({\n name: 'AccessCodeValidateControl',\n importPath: '@vtex/faststore/components',\n component: AccessCodeValidateControl,\n actions: { session: ['validateAccessCode'] },\n editor: { label: 'Access Code Validate', category: 'identity' },\n schema: {\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n submitLabel: { type: 'string', label: 'Submit label' },\n codeLength: { type: 'number', default: 6, label: 'Code length' },\n invalidCodeMessage: { type: 'string', label: 'Invalid code message' },\n blockedMessage: { type: 'string', label: 'Blocked message' },\n networkErrorMessage: { type: 'string', label: 'Network error message' },\n },\n guideline:\n 'Code-entry step: composes AccessCodeField and dispatches $actions.session.validateAccessCode with the Root-owned email and authenticationToken; a successful validate flips the session reactively and hides the Root. Must be inside AccessCodeSignInRoot. See access-code-sign-in.md.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Data-owning root of the access-code sign-in flow. Self-gates on two signals:\n * `useSession().state !== 'loggedIn'` (available from anonymous through identified; hides once\n * signed in) and `sessionAuthProviders.accessKeyAuthentication === true`. An unresolved fragment\n * read (`$data` still `null`) is treated as \"provider not enabled\" — the flow stays hidden rather\n * than flashing. Owns the `step`/`authenticationToken` stepper plus a locally-typed draft `email`\n * as local state, broadcasting all of it through context to its children. The draft email pre-fills\n * from `clientProfile.email` once (for an already-identified shopper) but a shopper edit is never\n * persisted anywhere by this family — it is purely the requestAccessCode/validateAccessCode\n * argument; the server sets clientProfileData.email as a side effect of a successful validate.\n * Reacts automatically once `useSession()` flips to `loggedIn` — no manual unmount call anywhere in\n * this family.\n */\nexport function AccessCodeSignInRoot({ $data: $dataProp, className, children, ...props }: AccessCodeSignInRootProps) {\n const $data = useDataOverride($dataProp);\n const { state } = useSession();\n\n const [step, setStep] = useState<AccessCodeSignInStep>('request');\n const [authenticationToken, setAuthenticationToken] = useState<string | null>(null);\n\n const startValidation = useCallback((token: string) => {\n setAuthenticationToken(token);\n setStep('validate');\n }, []);\n\n const savedEmail = $data?.clientProfile?.email ?? '';\n const [email, setEmailState] = useState('');\n const [touched, setTouched] = useState(false);\n\n // Pre-fill from the resolving/known profile email, but only while still at the request step and\n // until the shopper edits the draft — mirrors ClientProfileRoot's untouched-field re-seed, never\n // clobbering an in-progress edit. Freezing the re-seed once the flow leaves 'request' keeps a\n // mid-flow profile-cache change from diverging the email between requestAccessCode and\n // validateAccessCode (the server rejects a validate whose email differs from the request).\n useEffect(() => {\n if (step === 'request' && !touched) setEmailState(savedEmail);\n }, [savedEmail, touched, step]);\n\n const setEmail = useCallback((next: string) => {\n setTouched(true);\n setEmailState(next);\n }, []);\n\n const providerEnabled = $data?.sessionAuthProviders?.accessKeyAuthentication ?? false;\n\n const contextValue = useMemo<AccessCodeSignInContextValue>(\n () => ({ email, setEmail, step, authenticationToken, startValidation }),\n [email, setEmail, step, authenticationToken, startValidation],\n );\n\n const gated = state !== 'loggedIn' && providerEnabled;\n if (!gated) return null;\n\n return (\n <AccessCodeSignInContext value={contextValue}>\n <div {...props} className={className} data-fs-access-code-sign-in-root=\"\">\n {children}\n </div>\n </AccessCodeSignInContext>\n );\n}\n\n/**\n * The draft email input for the access-code flow. Renders only at the request step. Bound to the\n * Root's local `email`/`setEmail` — never a fragment, never a mutation of its own; typing here\n * never touches the cart. Format validation is `EmailField`'s own blur-triggered check (never\n * per-keystroke); `invalidEmailMessage` is only its fallback text, mirroring how\n * `EmailIdentificationControl` defers to the same particle for the same reason.\n */\nexport function AccessCodeEmailControl({\n label,\n placeholder,\n invalidEmailMessage,\n className,\n ...props\n}: AccessCodeEmailControlProps) {\n const { step, email, setEmail } = useAccessCodeSignIn();\n\n if (step !== 'request') return null;\n\n return (\n <div {...props} className={className} data-fs-access-code-email-control=\"\">\n <EmailField\n label={label}\n placeholder={placeholder}\n value={email}\n onValueChange={setEmail}\n errorMessage={invalidEmailMessage}\n />\n </div>\n );\n}\n\n/**\n * Renders only while the Root's stepper is at `step === 'request'`. Reads the draft email from\n * context and dispatches `$actions.session.requestAccessCode({ email })` once it is a valid,\n * non-empty address (no account-existence pre-check beyond that format gate). On a result with a\n * non-null token and no `userErrors`, advances the Root's stepper. Every `userErrors` entry renders\n * inline via a host-supplied, per-code message; the step never changes on error, so the shopper can\n * retry or abandon the method and continue as a guest.\n */\nexport function AccessCodeRequestControl({\n label,\n pendingLabel,\n sendLimitMessage,\n blockedMessage,\n networkErrorMessage,\n onRequested,\n $actions: $actionsProp,\n className,\n ...props\n}: AccessCodeRequestControlProps) {\n const $actions = useActionsOverride($actionsProp);\n const { step, email, startValidation } = useAccessCodeSignIn();\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<SessionUserError | null>(null);\n\n if (step !== 'request') return null;\n\n const canSubmit = !pending && isValidEmail(email);\n\n async function handleClick() {\n if (!canSubmit) return;\n setPending(true);\n setError(null);\n\n try {\n const result = (await $actions?.session?.requestAccessCode?.({ email })) ?? {\n authenticationToken: null,\n userErrors: [],\n };\n\n if (result.authenticationToken && result.userErrors.length === 0) {\n startValidation(result.authenticationToken);\n onRequested?.(result.authenticationToken);\n return;\n }\n\n const [firstError] = result.userErrors;\n if (firstError) setError(firstError);\n } finally {\n setPending(false);\n }\n }\n\n const message = error ? requestErrorMessage(error, { sendLimitMessage, blockedMessage, networkErrorMessage }) : null;\n\n return (\n <div\n {...props}\n className={className}\n data-fs-access-code-request-control=\"\"\n {...(pending ? { 'data-fs-access-code-request-pending': '' } : {})}\n >\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={!canSubmit}\n aria-busy={pending || undefined}\n data-fs-access-code-request-submit=\"\"\n data-fs-button=\"\"\n >\n {/* T38, FR-5: resolveAuthoredCopy so an authored empty/whitespace-only pendingLabel falls\n back to `label` instead of blanking the button while the request is in flight. */}\n {pending ? resolveAuthoredCopy(pendingLabel, label ?? '') : label}\n </button>\n {message ? (\n <div role=\"alert\" data-fs-access-code-request-error=\"\">\n {message}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Renders only while the Root's stepper is at `step === 'validate'`. Composes `AccessCodeField` for\n * the code value and a submit control, disabled below `codeLength` (a UX courtesy, never a\n * validity claim). Dispatches `$actions.session.validateAccessCode` with the email and\n * `authenticationToken` read from context. On a non-empty `userErrors` result, sets the field's\n * `invalid`/`errorMessage` from a host-supplied per-code message and retains the code/token so the\n * shopper can retry without re-requesting. On success it calls the optional `onValidated` and renders\n * no local \"success\" UI — `useSession()` flipping to `loggedIn` is what makes the whole Root\n * disappear.\n */\nexport function AccessCodeValidateControl({\n label,\n placeholder,\n submitLabel,\n codeLength = 6,\n invalidCodeMessage,\n blockedMessage,\n missingAuthContextMessage,\n networkErrorMessage,\n onValidated,\n $actions: $actionsProp,\n className,\n ...props\n}: AccessCodeValidateControlProps) {\n const $actions = useActionsOverride($actionsProp);\n const { step, email, authenticationToken } = useAccessCodeSignIn();\n const [code, setCode] = useState('');\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<SessionUserError | null>(null);\n\n if (step !== 'validate') return null;\n\n const canSubmit = !pending && code.length >= codeLength && authenticationToken != null;\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!canSubmit || authenticationToken == null) return;\n\n setPending(true);\n setError(null);\n\n try {\n const errors =\n (await $actions?.session?.validateAccessCode?.({ email, accessCode: code, authenticationToken })) ?? [];\n\n const [firstError] = errors;\n if (firstError) {\n setError(firstError);\n return;\n }\n } finally {\n setPending(false);\n }\n\n onValidated?.();\n }\n\n function handleCodeChange(next: string) {\n setCode(next);\n if (error) setError(null);\n }\n\n const errorMessage = error\n ? validateErrorMessage(error, {\n invalidCodeMessage,\n blockedMessage,\n missingAuthContextMessage,\n networkErrorMessage,\n })\n : undefined;\n\n return (\n <div\n {...props}\n className={className}\n data-fs-access-code-validate-control=\"\"\n {...(pending ? { 'data-fs-access-code-validate-pending': '' } : {})}\n >\n <form onSubmit={handleSubmit}>\n <AccessCodeField\n label={label}\n placeholder={placeholder}\n value={code}\n maxLength={codeLength}\n onValueChange={handleCodeChange}\n disabled={pending}\n invalid={error != null}\n errorMessage={errorMessage}\n />\n <button\n type=\"submit\"\n disabled={!canSubmit}\n aria-busy={pending || undefined}\n data-fs-access-code-validate-submit=\"\"\n data-fs-button=\"\"\n >\n {submitLabel}\n </button>\n </form>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const AccessCodeSignIn = {\n Root: AccessCodeSignInRoot,\n Email: AccessCodeEmailControl,\n Request: AccessCodeRequestControl,\n Validate: AccessCodeValidateControl,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ContentValue } from "../../core/dist/define-content-type.js";
|
|
2
|
+
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
5
|
+
//#region src/matter/account-access-gate/account-access-gate.d.ts
|
|
6
|
+
export declare const AccountAccessGateContent: {
|
|
7
|
+
readonly id: "account-access-gate";
|
|
8
|
+
readonly title: "Account Access Gate";
|
|
9
|
+
readonly description: "What the shopper is told while the account resolves and when it will not open for them.";
|
|
10
|
+
readonly schema: {
|
|
11
|
+
readonly type: "object";
|
|
12
|
+
readonly additionalProperties: false;
|
|
13
|
+
readonly properties: {
|
|
14
|
+
readonly loadingLabel: {
|
|
15
|
+
readonly type: "string";
|
|
16
|
+
readonly minLength: 1;
|
|
17
|
+
readonly title: "Loading announcement";
|
|
18
|
+
};
|
|
19
|
+
readonly expiredMessage: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly minLength: 1;
|
|
22
|
+
readonly title: "Session ended message";
|
|
23
|
+
};
|
|
24
|
+
readonly signInIncompleteMessage: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
readonly minLength: 1;
|
|
27
|
+
readonly title: "Sign-in did not complete message";
|
|
28
|
+
};
|
|
29
|
+
readonly signInLabel: {
|
|
30
|
+
readonly type: "string";
|
|
31
|
+
readonly minLength: 1;
|
|
32
|
+
readonly title: "Sign-in link label";
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** Why the account will not open, as a bounded value: a style target and the choice of message. */
|
|
38
|
+
export type AccountAccessGateNoticeReason = 'expired' | 'sign-in-incomplete';
|
|
39
|
+
/** Props for {@link AccountAccessGate}. It renders a different element per outcome and forwards no
|
|
40
|
+
* element props of its own; each rendered part carries its own. */
|
|
41
|
+
export type AccountAccessGateProps = {
|
|
42
|
+
/** Text announced while the session resolves. */
|
|
43
|
+
loadingLabel?: string;
|
|
44
|
+
/** What a shopper whose session ended mid-visit is told. */
|
|
45
|
+
expiredMessage?: string;
|
|
46
|
+
/** What a shopper who came back from sign-in still signed out is told. */
|
|
47
|
+
signInIncompleteMessage?: string;
|
|
48
|
+
/** Label of the link out of either notice. Without it no link renders — a focusable link a screen
|
|
49
|
+
* reader announces as its bare URL is worse than no control at all. */
|
|
50
|
+
signInLabel?: string;
|
|
51
|
+
/** Merchant-authored fields, sliced from the layout content entry; win over the props field by field
|
|
52
|
+
* when present. Absent in a non-CMS store — the props are the fallback. */
|
|
53
|
+
$content?: ContentValue<typeof AccountAccessGateContent>;
|
|
54
|
+
/** The account. Rendered only for a signed-in shopper. */
|
|
55
|
+
children?: ReactNode;
|
|
56
|
+
};
|
|
57
|
+
/** Props for {@link AccountAccessGateLoading}. Extends native `<output>` attributes. */
|
|
58
|
+
export type AccountAccessGateLoadingProps = ComponentPropsWithRef<'output'>;
|
|
59
|
+
/** Props for {@link AccountAccessGateNotice}. Extends native `<output>` attributes. */
|
|
60
|
+
export type AccountAccessGateNoticeProps = Omit<ComponentPropsWithRef<'output'>, 'children'> & {
|
|
61
|
+
reason: AccountAccessGateNoticeReason;
|
|
62
|
+
/** The message for this reason. Nothing renders in its place when unauthored. */
|
|
63
|
+
message?: string;
|
|
64
|
+
/** Where the link out leads, built by the gate from the page the shopper is on. */
|
|
65
|
+
signInHref: string;
|
|
66
|
+
/** Label of the link out. No label, no link. */
|
|
67
|
+
signInLabel?: string;
|
|
68
|
+
};
|
|
69
|
+
export declare const AccountAccessGateDefinition: ComponentDefinition<AccountAccessGateProps>;
|
|
70
|
+
export declare function AccountAccessGate({ loadingLabel, expiredMessage, signInIncompleteMessage, signInLabel, $content, children }: Readonly<AccountAccessGateProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
71
|
+
/**
|
|
72
|
+
* The look of a session that has not answered yet. A real part with its own hook rather than an ad hoc
|
|
73
|
+
* spinner, so a store can style it and a story can render it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function AccountAccessGateLoading({ className, children, ...props }: AccountAccessGateLoadingProps): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
/**
|
|
77
|
+
* What renders in the account's place when it will not open: the message for this reason, and a link
|
|
78
|
+
* the shopper may take back into sign-in.
|
|
79
|
+
*
|
|
80
|
+
* The link is the whole of the escape from a sign-in loop. The gate stops redirecting after one
|
|
81
|
+
* automatic hop, so the next attempt is a click rather than a bounce — and a shopper who keeps landing
|
|
82
|
+
* here at least lands on a page that says so.
|
|
83
|
+
*/
|
|
84
|
+
export declare function AccountAccessGateNotice({ reason, message, signInHref, signInLabel, className, ...props }: AccountAccessGateNoticeProps): import("react/jsx-runtime").JSX.Element;
|
|
85
|
+
//#endregion
|
|
86
|
+
//# sourceMappingURL=account-access-gate.d.ts.map
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import { defineContentType } from "../../core/dist/define-content-type.js";
|
|
4
|
+
import "../../core/dist/index.js";
|
|
5
|
+
import { useSession } from "../session/session.js";
|
|
6
|
+
import { resolveSignInHref } from "../../utils/safe-href.js";
|
|
7
|
+
import { NavigationLink } from "../../particles/navigation-link/navigation-link.js";
|
|
8
|
+
import { useSessionRevalidation } from "../../sdk/session/use-session-revalidation.js";
|
|
9
|
+
import { useSignOutIntent, withdrawSignOutIntent } from "./sign-out-intent.js";
|
|
10
|
+
import { useEffect, useRef, useState } from "react";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
13
|
+
import { flushSync } from "react-dom";
|
|
14
|
+
//#region src/matter/account-access-gate/account-access-gate.tsx
|
|
15
|
+
const AccountAccessGateContent = defineContentType({
|
|
16
|
+
id: "account-access-gate",
|
|
17
|
+
title: "Account Access Gate",
|
|
18
|
+
description: "What the shopper is told while the account resolves and when it will not open for them.",
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
loadingLabel: {
|
|
24
|
+
type: "string",
|
|
25
|
+
minLength: 1,
|
|
26
|
+
title: "Loading announcement"
|
|
27
|
+
},
|
|
28
|
+
expiredMessage: {
|
|
29
|
+
type: "string",
|
|
30
|
+
minLength: 1,
|
|
31
|
+
title: "Session ended message"
|
|
32
|
+
},
|
|
33
|
+
signInIncompleteMessage: {
|
|
34
|
+
type: "string",
|
|
35
|
+
minLength: 1,
|
|
36
|
+
title: "Sign-in did not complete message"
|
|
37
|
+
},
|
|
38
|
+
signInLabel: {
|
|
39
|
+
type: "string",
|
|
40
|
+
minLength: 1,
|
|
41
|
+
title: "Sign-in link label"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const AccountAccessGateDefinition = defineComponent({
|
|
47
|
+
name: "AccountAccessGate",
|
|
48
|
+
importPath: "@vtex/faststore/components",
|
|
49
|
+
component: AccountAccessGate,
|
|
50
|
+
content: AccountAccessGateContent,
|
|
51
|
+
editor: {
|
|
52
|
+
label: "Account Access Gate",
|
|
53
|
+
category: "identity"
|
|
54
|
+
},
|
|
55
|
+
requiresContext: "session",
|
|
56
|
+
schema: {
|
|
57
|
+
loadingLabel: {
|
|
58
|
+
type: "string",
|
|
59
|
+
label: "Loading Announcement"
|
|
60
|
+
},
|
|
61
|
+
expiredMessage: {
|
|
62
|
+
type: "string",
|
|
63
|
+
label: "Session Ended Message"
|
|
64
|
+
},
|
|
65
|
+
signInIncompleteMessage: {
|
|
66
|
+
type: "string",
|
|
67
|
+
label: "Sign-in Did Not Complete Message"
|
|
68
|
+
},
|
|
69
|
+
signInLabel: {
|
|
70
|
+
type: "string",
|
|
71
|
+
label: "Sign-in Link Label"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
guideline: "The account's single access decision: place it once in the account layout, wrapping the body, and every page that declares that layout inherits it — never restate it per section. It renders its children only for a logged-in shopper; it shows a composed loading look while the session resolves, replaces the current history entry with the sign-in destination for a visitor who arrived signed out (identified counts as signed out), tells a shopper whose session ended mid-visit rather than bouncing them, and stands down while AccountMenuSignOut navigates. It redirects at most once per visit: a shopper who comes back from sign-in still signed out gets a message and a link rather than a second hop. Must be inside SessionRoot, which the same layout hosts. Never place the /login route under it — the gate redirects there, and a gated /login would redirect to itself."
|
|
75
|
+
});
|
|
76
|
+
/** The one automatic hop the gate has already spent, surviving the full document load out to the
|
|
77
|
+
* platform and back, and no further than this tab. */
|
|
78
|
+
const SIGN_IN_ATTEMPT_KEY = "fs::account-access-gate::sign-in-attempt";
|
|
79
|
+
function AccountAccessGate({ loadingLabel, expiredMessage, signInIncompleteMessage, signInLabel, $content, children }) {
|
|
80
|
+
const { state, loading } = useSession();
|
|
81
|
+
const signingOut = useSignOutIntent();
|
|
82
|
+
const pathname = usePathname();
|
|
83
|
+
const router = useRouter();
|
|
84
|
+
const revalidate = useSessionRevalidation();
|
|
85
|
+
const [wasSignedIn, setWasSignedIn] = useState(false);
|
|
86
|
+
const [unconfirmed, setUnconfirmed] = useState(false);
|
|
87
|
+
const [revalidating, setRevalidating] = useState(false);
|
|
88
|
+
const restoreGeneration = useRef(0);
|
|
89
|
+
const [hopSpent, setHopSpent] = useState(null);
|
|
90
|
+
const [search, setSearch] = useState(null);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setSearch(globalThis.location.search);
|
|
93
|
+
});
|
|
94
|
+
const returnPath = pathname === null ? null : `${pathname}${search ?? ""}`;
|
|
95
|
+
const outcome = resolveOutcome({
|
|
96
|
+
signingOut,
|
|
97
|
+
unconfirmed,
|
|
98
|
+
revalidating,
|
|
99
|
+
loading,
|
|
100
|
+
state,
|
|
101
|
+
wasSignedIn,
|
|
102
|
+
hopSpent
|
|
103
|
+
});
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
switch (outcome) {
|
|
106
|
+
case "account":
|
|
107
|
+
setWasSignedIn(true);
|
|
108
|
+
setUnconfirmed(false);
|
|
109
|
+
clearSignInAttempt();
|
|
110
|
+
return;
|
|
111
|
+
case "expired":
|
|
112
|
+
case "sign-in-incomplete":
|
|
113
|
+
clearSignInAttempt();
|
|
114
|
+
return;
|
|
115
|
+
case "signing-in": {
|
|
116
|
+
if (hopSpent !== null || search === null) return;
|
|
117
|
+
if (readSignInAttempt() !== null) {
|
|
118
|
+
setHopSpent(true);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const destination = resolveSignInHref(returnPath);
|
|
122
|
+
recordSignInAttempt(destination);
|
|
123
|
+
setHopSpent(false);
|
|
124
|
+
router.replace(destination);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
case "stand-down":
|
|
128
|
+
case "loading": return;
|
|
129
|
+
default: assertUnreachable(outcome);
|
|
130
|
+
}
|
|
131
|
+
}, [
|
|
132
|
+
outcome,
|
|
133
|
+
hopSpent,
|
|
134
|
+
search,
|
|
135
|
+
returnPath,
|
|
136
|
+
router
|
|
137
|
+
]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
function handlePageHide(event) {
|
|
140
|
+
if (!event.persisted) return;
|
|
141
|
+
flushSync(() => {
|
|
142
|
+
setRevalidating(true);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function handlePageShow(event) {
|
|
146
|
+
if (!event.persisted) return;
|
|
147
|
+
const generation = restoreGeneration.current + 1;
|
|
148
|
+
restoreGeneration.current = generation;
|
|
149
|
+
withdrawSignOutIntent();
|
|
150
|
+
setUnconfirmed(false);
|
|
151
|
+
setRevalidating(true);
|
|
152
|
+
revalidate().then((result) => {
|
|
153
|
+
if (generation !== restoreGeneration.current) return;
|
|
154
|
+
setRevalidating(false);
|
|
155
|
+
switch (result) {
|
|
156
|
+
case "failed":
|
|
157
|
+
setUnconfirmed(true);
|
|
158
|
+
return;
|
|
159
|
+
case "revalidated":
|
|
160
|
+
case "unavailable": return;
|
|
161
|
+
default: assertUnreachable(result);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
globalThis.addEventListener("pagehide", handlePageHide);
|
|
166
|
+
globalThis.addEventListener("pageshow", handlePageShow);
|
|
167
|
+
return () => {
|
|
168
|
+
globalThis.removeEventListener("pagehide", handlePageHide);
|
|
169
|
+
globalThis.removeEventListener("pageshow", handlePageShow);
|
|
170
|
+
};
|
|
171
|
+
}, [revalidate]);
|
|
172
|
+
useEffect(() => withdrawSignOutIntent, []);
|
|
173
|
+
const entryLoadingLabel = $content?.loadingLabel?.trim() ? $content.loadingLabel : loadingLabel;
|
|
174
|
+
const entryExpiredMessage = $content?.expiredMessage?.trim() ? $content.expiredMessage : expiredMessage;
|
|
175
|
+
const entryIncompleteMessage = $content?.signInIncompleteMessage?.trim() ? $content.signInIncompleteMessage : signInIncompleteMessage;
|
|
176
|
+
const entrySignInLabel = $content?.signInLabel?.trim() ? $content.signInLabel : signInLabel;
|
|
177
|
+
switch (outcome) {
|
|
178
|
+
case "stand-down":
|
|
179
|
+
case "signing-in": return null;
|
|
180
|
+
case "loading": return /* @__PURE__ */ jsx(AccountAccessGateLoading, {
|
|
181
|
+
"data-fs-account-access-gate": "",
|
|
182
|
+
children: entryLoadingLabel
|
|
183
|
+
});
|
|
184
|
+
case "account": return /* @__PURE__ */ jsx(Fragment, { children });
|
|
185
|
+
case "expired":
|
|
186
|
+
case "sign-in-incomplete": return /* @__PURE__ */ jsx(AccountAccessGateNotice, {
|
|
187
|
+
"data-fs-account-access-gate": "",
|
|
188
|
+
reason: outcome === "expired" ? "expired" : "sign-in-incomplete",
|
|
189
|
+
message: outcome === "expired" ? entryExpiredMessage : entryIncompleteMessage,
|
|
190
|
+
signInHref: resolveSignInHref(returnPath),
|
|
191
|
+
signInLabel: entrySignInLabel
|
|
192
|
+
});
|
|
193
|
+
default: return assertUnreachable(outcome);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The whole decision, as one ordered read of the gate's inputs.
|
|
198
|
+
*
|
|
199
|
+
* `signingOut` outranks everything, including a session still reading `loggedIn`: the sign-out records
|
|
200
|
+
* its intent before it awaits the mutation, and the frame between the payload landing and the document
|
|
201
|
+
* load starting is exactly the one this ordering protects.
|
|
202
|
+
*
|
|
203
|
+
* `loading` outranks the tri-state for the reason `AccountEntryPoint` gives: an unresolved session
|
|
204
|
+
* reports `anonymous`, so reading the state first would bounce a shopper who is signed in. A re-read
|
|
205
|
+
* in flight reads as the same wait: the session it is checking is one the gate has stopped believing.
|
|
206
|
+
*/
|
|
207
|
+
function resolveOutcome({ signingOut, unconfirmed, revalidating, loading, state, wasSignedIn, hopSpent }) {
|
|
208
|
+
if (signingOut) return "stand-down";
|
|
209
|
+
if (unconfirmed) return wasSignedIn ? "expired" : "sign-in-incomplete";
|
|
210
|
+
if (revalidating || loading) return "loading";
|
|
211
|
+
if (state === "loggedIn") return "account";
|
|
212
|
+
if (wasSignedIn) return "expired";
|
|
213
|
+
return hopSpent === true ? "sign-in-incomplete" : "signing-in";
|
|
214
|
+
}
|
|
215
|
+
function assertUnreachable(outcome) {
|
|
216
|
+
throw new Error(`[AccountAccessGate] unhandled outcome: ${String(outcome)}`);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The look of a session that has not answered yet. A real part with its own hook rather than an ad hoc
|
|
220
|
+
* spinner, so a store can style it and a story can render it.
|
|
221
|
+
*/
|
|
222
|
+
function AccountAccessGateLoading({ className, children, ...props }) {
|
|
223
|
+
return /* @__PURE__ */ jsx("output", {
|
|
224
|
+
...props,
|
|
225
|
+
"aria-live": "polite",
|
|
226
|
+
className,
|
|
227
|
+
"data-fs-account-access-gate-loading": "",
|
|
228
|
+
children
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What renders in the account's place when it will not open: the message for this reason, and a link
|
|
233
|
+
* the shopper may take back into sign-in.
|
|
234
|
+
*
|
|
235
|
+
* The link is the whole of the escape from a sign-in loop. The gate stops redirecting after one
|
|
236
|
+
* automatic hop, so the next attempt is a click rather than a bounce — and a shopper who keeps landing
|
|
237
|
+
* here at least lands on a page that says so.
|
|
238
|
+
*/
|
|
239
|
+
function AccountAccessGateNotice({ reason, message, signInHref, signInLabel, className, ...props }) {
|
|
240
|
+
return /* @__PURE__ */ jsxs("output", {
|
|
241
|
+
...props,
|
|
242
|
+
"aria-live": "polite",
|
|
243
|
+
className,
|
|
244
|
+
"data-fs-account-access-gate-notice": "",
|
|
245
|
+
"data-fs-account-access-gate-notice-reason": reason,
|
|
246
|
+
children: [message ? /* @__PURE__ */ jsx("span", {
|
|
247
|
+
"data-fs-account-access-gate-notice-message": "",
|
|
248
|
+
children: message
|
|
249
|
+
}) : null, signInLabel?.trim() ? /* @__PURE__ */ jsx(NavigationLink, {
|
|
250
|
+
href: signInHref,
|
|
251
|
+
prefetch: false,
|
|
252
|
+
"data-fs-account-access-gate-notice-action": "",
|
|
253
|
+
children: signInLabel
|
|
254
|
+
}) : null]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Session storage, guarded at every access. A browser that refuses it — a shopper blocking site data —
|
|
259
|
+
* throws rather than returning null, and a gate that cannot read its own record still has a shopper to
|
|
260
|
+
* get to sign-in. A refusal therefore reads as "no record", which spends the hop; the cost is that the
|
|
261
|
+
* loop protection is absent in that browser, which is the lesser of the two failures.
|
|
262
|
+
*/
|
|
263
|
+
function readSignInAttempt() {
|
|
264
|
+
try {
|
|
265
|
+
return globalThis.sessionStorage.getItem(SIGN_IN_ATTEMPT_KEY);
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function recordSignInAttempt(destination) {
|
|
271
|
+
try {
|
|
272
|
+
globalThis.sessionStorage.setItem(SIGN_IN_ATTEMPT_KEY, destination);
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
function clearSignInAttempt() {
|
|
276
|
+
try {
|
|
277
|
+
globalThis.sessionStorage.removeItem(SIGN_IN_ATTEMPT_KEY);
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
export { AccountAccessGate, AccountAccessGateContent, AccountAccessGateDefinition, AccountAccessGateLoading, AccountAccessGateNotice };
|
|
282
|
+
|
|
283
|
+
//# sourceMappingURL=account-access-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-access-gate.js","names":[],"sources":["../../../src/matter/account-access-gate/account-access-gate.tsx"],"sourcesContent":["'use client';\n\nimport { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport { usePathname, useRouter } from 'next/navigation';\nimport { type ComponentPropsWithRef, type ReactNode, useEffect, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { NavigationLink } from '../../particles/navigation-link/navigation-link.js';\nimport { useSessionRevalidation } from '../../sdk/session/use-session-revalidation.js';\nimport { resolveSignInHref } from '../../utils/safe-href.js';\nimport { type SessionState, useSession } from '../session/session.js';\nimport { useSignOutIntent, withdrawSignOutIntent } from './sign-out-intent.js';\n\n// Nothing is required, following the shipped identity components: an entry may author any subset, and\n// a slice missing a required property is shape-invalid, which drops the whole element rather than the\n// one field.\nexport const AccountAccessGateContent = defineContentType({\n id: 'account-access-gate',\n title: 'Account Access Gate',\n description: 'What the shopper is told while the account resolves and when it will not open for them.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n loadingLabel: { type: 'string', minLength: 1, title: 'Loading announcement' },\n expiredMessage: { type: 'string', minLength: 1, title: 'Session ended message' },\n signInIncompleteMessage: { type: 'string', minLength: 1, title: 'Sign-in did not complete message' },\n signInLabel: { type: 'string', minLength: 1, title: 'Sign-in link label' },\n },\n },\n});\n\n/** Why the account will not open, as a bounded value: a style target and the choice of message. */\nexport type AccountAccessGateNoticeReason = 'expired' | 'sign-in-incomplete';\n\n/** Props for {@link AccountAccessGate}. It renders a different element per outcome and forwards no\n * element props of its own; each rendered part carries its own. */\nexport type AccountAccessGateProps = {\n /** Text announced while the session resolves. */\n loadingLabel?: string;\n /** What a shopper whose session ended mid-visit is told. */\n expiredMessage?: string;\n /** What a shopper who came back from sign-in still signed out is told. */\n signInIncompleteMessage?: string;\n /** Label of the link out of either notice. Without it no link renders — a focusable link a screen\n * reader announces as its bare URL is worse than no control at all. */\n signInLabel?: string;\n /** Merchant-authored fields, sliced from the layout content entry; win over the props field by field\n * when present. Absent in a non-CMS store — the props are the fallback. */\n $content?: ContentValue<typeof AccountAccessGateContent>;\n /** The account. Rendered only for a signed-in shopper. */\n children?: ReactNode;\n};\n\n/** Props for {@link AccountAccessGateLoading}. Extends native `<output>` attributes. */\nexport type AccountAccessGateLoadingProps = ComponentPropsWithRef<'output'>;\n\n/** Props for {@link AccountAccessGateNotice}. Extends native `<output>` attributes. */\nexport type AccountAccessGateNoticeProps = Omit<ComponentPropsWithRef<'output'>, 'children'> & {\n reason: AccountAccessGateNoticeReason;\n /** The message for this reason. Nothing renders in its place when unauthored. */\n message?: string;\n /** Where the link out leads, built by the gate from the page the shopper is on. */\n signInHref: string;\n /** Label of the link out. No label, no link. */\n signInLabel?: string;\n};\n\nexport const AccountAccessGateDefinition = defineComponent<AccountAccessGateProps>({\n name: 'AccountAccessGate',\n importPath: '@vtex/faststore/components',\n component: AccountAccessGate,\n content: AccountAccessGateContent,\n editor: { label: 'Account Access Gate', category: 'identity' },\n requiresContext: 'session',\n schema: {\n // Mirrored from the content model on purpose, as every content-bearing component in the library\n // does: the entry wins when published, and this is what a store without a CMS authors.\n loadingLabel: { type: 'string', label: 'Loading Announcement' },\n expiredMessage: { type: 'string', label: 'Session Ended Message' },\n signInIncompleteMessage: { type: 'string', label: 'Sign-in Did Not Complete Message' },\n signInLabel: { type: 'string', label: 'Sign-in Link Label' },\n },\n guideline:\n \"The account's single access decision: place it once in the account layout, wrapping the body, and every page that declares that layout inherits it — never restate it per section. It renders its children only for a logged-in shopper; it shows a composed loading look while the session resolves, replaces the current history entry with the sign-in destination for a visitor who arrived signed out (identified counts as signed out), tells a shopper whose session ended mid-visit rather than bouncing them, and stands down while AccountMenuSignOut navigates. It redirects at most once per visit: a shopper who comes back from sign-in still signed out gets a message and a link rather than a second hop. Must be inside SessionRoot, which the same layout hosts. Never place the /login route under it — the gate redirects there, and a gated /login would redirect to itself.\",\n});\n\n/** The one automatic hop the gate has already spent, surviving the full document load out to the\n * platform and back, and no further than this tab. */\nconst SIGN_IN_ATTEMPT_KEY = 'fs::account-access-gate::sign-in-attempt';\n\n/**\n * What the gate renders, in precedence order. `signing-in` covers both the frame that decides whether\n * the hop is still available and the frame after the replace is issued: they render the same nothing,\n * and only the effect tells them apart.\n */\ntype AccountAccessGateOutcome = 'stand-down' | 'expired' | 'loading' | 'account' | 'sign-in-incomplete' | 'signing-in';\n\nexport function AccountAccessGate({\n loadingLabel,\n expiredMessage,\n signInIncompleteMessage,\n signInLabel,\n $content,\n children,\n}: Readonly<AccountAccessGateProps>) {\n const { state, loading } = useSession();\n const signingOut = useSignOutIntent();\n const pathname = usePathname();\n const router = useRouter();\n const revalidate = useSessionRevalidation();\n\n // Whether this gate has ever resolved to a signed-in shopper. It is what separates a session that\n // ended from a visitor who arrived signed out — the tri-state spells both `anonymous`.\n const [wasSignedIn, setWasSignedIn] = useState(false);\n // A revalidation that could not answer. It outranks `loading` because the discard that preceded it\n // is what leaves the session unresolved, so nothing else is coming to end that wait.\n const [unconfirmed, setUnconfirmed] = useState(false);\n // A re-read in flight. The gate withholds the account for itself rather than waiting to be told:\n // discarding the cached session leaves the data layer unresolved, but it reaches the gate only once\n // the watched query has re-read, and the account must be gone in the frame the restore arrives.\n const [revalidating, setRevalidating] = useState(false);\n // Which re-read the gate is still listening to. Two restores in a row put two answers in flight, and\n // the older one may land last.\n const restoreGeneration = useRef(0);\n // `null` until the arrival path has read the record. Read from an effect rather than from the render\n // body: session storage does not exist on the server, and a value that appeared between the server\n // pass and hydration would change what renders underneath it.\n const [hopSpent, setHopSpent] = useState<boolean | null>(null);\n // `null` until the document has been read, for the same two reasons, and read from the document\n // rather than through `useSearchParams()`, which would require a `Suspense` boundary the generator\n // does not emit on a prerendered route — the reason `SignInRedirect` gives for reading it this way.\n const [search, setSearch] = useState<string | null>(null);\n\n // Deliberately on every commit rather than on mount: `usePathname()` re-renders the gate when the\n // path changes but says nothing about the query, and a shopper who pages through their orders\n // changes only the query. Setting the same string is a bail-out, so this does not loop.\n useEffect(() => {\n setSearch(globalThis.location.search);\n });\n\n // Where the shopper comes back to. `usePathname()` yields the path alone, so a gate that stopped\n // there would return someone reading `/account/orders?page=2` to the first page of it.\n const returnPath = pathname === null ? null : `${pathname}${search ?? ''}`;\n\n const outcome = resolveOutcome({ signingOut, unconfirmed, revalidating, loading, state, wasSignedIn, hopSpent });\n\n useEffect(() => {\n switch (outcome) {\n case 'account':\n setWasSignedIn(true);\n setUnconfirmed(false);\n clearSignInAttempt();\n return;\n case 'expired':\n case 'sign-in-incomplete':\n // Every outcome that is not the hop returns it: a shopper who signs in, browses, and comes\n // back later arrives fresh rather than carrying a spent attempt from an earlier one.\n clearSignInAttempt();\n return;\n case 'signing-in': {\n // One more frame before deciding, until the document has been read: a hop recorded against the\n // bare path is the one the shopper comes back to, and there is no second chance at it.\n if (hopSpent !== null || search === null) {\n return;\n }\n if (readSignInAttempt() !== null) {\n setHopSpent(true);\n return;\n }\n const destination = resolveSignInHref(returnPath);\n recordSignInAttempt(destination);\n setHopSpent(false);\n // Replace rather than push: back from the platform's login should reach the page the shopper\n // came from, not the account that threw them at it.\n // react-doctor-disable-next-line nextjs-no-client-side-redirect -- the session resolves on the client, so the server has nothing to decide with\n router.replace(destination);\n return;\n }\n case 'stand-down':\n case 'loading':\n return;\n default:\n assertUnreachable(outcome);\n }\n }, [outcome, hopSpent, search, returnPath, router]);\n\n useEffect(() => {\n function handlePageHide(event: PageTransitionEvent): void {\n if (!event.persisted) {\n return;\n }\n // What the browser freezes is the DOM, and a restore puts that same DOM back before any script\n // runs. Standing the account down on the way out rather than on the way back is what keeps the\n // restored document from showing it in the frames before `pageshow` can decide again.\n flushSync(() => {\n setRevalidating(true);\n });\n }\n\n function handlePageShow(event: PageTransitionEvent): void {\n // An ordinary load re-decides by mounting. Only a restore resumes a heap that already holds an\n // answer, and `persisted` is the browser's own word for it.\n if (!event.persisted) {\n return;\n }\n const generation = restoreGeneration.current + 1;\n restoreGeneration.current = generation;\n // The navigation this authorized has already happened; a restored document holding it would\n // stand down forever. Withdrawn before the re-read so the gate is deciding again by the time\n // the answer lands.\n withdrawSignOutIntent();\n setUnconfirmed(false);\n setRevalidating(true);\n void revalidate().then((result) => {\n // A later restore has already asked the same question, and its answer is the one that counts.\n // This one describes a session the gate has since discarded a second time.\n if (generation !== restoreGeneration.current) {\n return;\n }\n setRevalidating(false);\n switch (result) {\n case 'failed':\n setUnconfirmed(true);\n return;\n case 'revalidated':\n case 'unavailable':\n // Nothing left to distrust: either the session was re-read, or there was no cached\n // session to discard in the first place and the one in the tree came from a prop.\n return;\n default:\n assertUnreachable(result);\n }\n });\n }\n\n globalThis.addEventListener('pagehide', handlePageHide);\n globalThis.addEventListener('pageshow', handlePageShow);\n return () => {\n globalThis.removeEventListener('pagehide', handlePageHide);\n globalThis.removeEventListener('pageshow', handlePageShow);\n };\n }, [revalidate]);\n\n // The intent authorizes one transition and this gate is its only reader, so it must not survive the\n // gate. A sign-out that navigates client-side takes the account layout down without reloading the\n // document, and nothing else would ever take the flag back: the next account page the shopper opens\n // would mount into a standing intent and render nothing at all, with no error to explain it.\n useEffect(() => withdrawSignOutIntent, []);\n\n // Blank entry fields fall through, so clearing one in the Admin reads as unauthored.\n const entryLoadingLabel = $content?.loadingLabel?.trim() ? $content.loadingLabel : loadingLabel;\n const entryExpiredMessage = $content?.expiredMessage?.trim() ? $content.expiredMessage : expiredMessage;\n const entryIncompleteMessage = $content?.signInIncompleteMessage?.trim()\n ? $content.signInIncompleteMessage\n : signInIncompleteMessage;\n const entrySignInLabel = $content?.signInLabel?.trim() ? $content.signInLabel : signInLabel;\n\n switch (outcome) {\n case 'stand-down':\n case 'signing-in':\n return null;\n case 'loading':\n return (\n // The part carries its own hook; the component's identity travels from here, which is how a\n // store targets it by name the way every other component is targeted.\n <AccountAccessGateLoading data-fs-account-access-gate=\"\">{entryLoadingLabel}</AccountAccessGateLoading>\n );\n case 'account':\n return <>{children}</>;\n case 'expired':\n case 'sign-in-incomplete':\n return (\n <AccountAccessGateNotice\n data-fs-account-access-gate=\"\"\n reason={outcome === 'expired' ? 'expired' : 'sign-in-incomplete'}\n message={outcome === 'expired' ? entryExpiredMessage : entryIncompleteMessage}\n signInHref={resolveSignInHref(returnPath)}\n signInLabel={entrySignInLabel}\n />\n );\n default:\n return assertUnreachable(outcome);\n }\n}\n\n/**\n * The whole decision, as one ordered read of the gate's inputs.\n *\n * `signingOut` outranks everything, including a session still reading `loggedIn`: the sign-out records\n * its intent before it awaits the mutation, and the frame between the payload landing and the document\n * load starting is exactly the one this ordering protects.\n *\n * `loading` outranks the tri-state for the reason `AccountEntryPoint` gives: an unresolved session\n * reports `anonymous`, so reading the state first would bounce a shopper who is signed in. A re-read\n * in flight reads as the same wait: the session it is checking is one the gate has stopped believing.\n */\nfunction resolveOutcome({\n signingOut,\n unconfirmed,\n revalidating,\n loading,\n state,\n wasSignedIn,\n hopSpent,\n}: {\n signingOut: boolean;\n unconfirmed: boolean;\n revalidating: boolean;\n loading: boolean;\n state: SessionState;\n wasSignedIn: boolean;\n hopSpent: boolean | null;\n}): AccountAccessGateOutcome {\n if (signingOut) {\n return 'stand-down';\n }\n // A re-read that could not answer says nothing about how the shopper got here, so the notice keeps\n // the history the gate already has. Telling someone who never reached the account that their session\n // ended names an event that never happened; the incomplete sign-in is the one they actually had.\n if (unconfirmed) {\n return wasSignedIn ? 'expired' : 'sign-in-incomplete';\n }\n if (revalidating || loading) {\n return 'loading';\n }\n if (state === 'loggedIn') {\n return 'account';\n }\n if (wasSignedIn) {\n return 'expired';\n }\n return hopSpent === true ? 'sign-in-incomplete' : 'signing-in';\n}\n\nfunction assertUnreachable(outcome: never): never {\n throw new Error(`[AccountAccessGate] unhandled outcome: ${String(outcome)}`);\n}\n\n/**\n * The look of a session that has not answered yet. A real part with its own hook rather than an ad hoc\n * spinner, so a store can style it and a story can render it.\n */\nexport function AccountAccessGateLoading({ className, children, ...props }: AccountAccessGateLoadingProps) {\n return (\n <output {...props} aria-live=\"polite\" className={className} data-fs-account-access-gate-loading=\"\">\n {children}\n </output>\n );\n}\n\n/**\n * What renders in the account's place when it will not open: the message for this reason, and a link\n * the shopper may take back into sign-in.\n *\n * The link is the whole of the escape from a sign-in loop. The gate stops redirecting after one\n * automatic hop, so the next attempt is a click rather than a bounce — and a shopper who keeps landing\n * here at least lands on a page that says so.\n */\nexport function AccountAccessGateNotice({\n reason,\n message,\n signInHref,\n signInLabel,\n className,\n ...props\n}: AccountAccessGateNoticeProps) {\n return (\n <output\n {...props}\n aria-live=\"polite\"\n className={className}\n data-fs-account-access-gate-notice=\"\"\n data-fs-account-access-gate-notice-reason={reason}\n >\n {message ? <span data-fs-account-access-gate-notice-message=\"\">{message}</span> : null}\n {signInLabel?.trim() ? (\n <NavigationLink\n href={signInHref}\n // The destination is a trampoline that redirects on mount, so the default prefetch would\n // fire that GET from a page the shopper may never leave.\n prefetch={false}\n data-fs-account-access-gate-notice-action=\"\"\n >\n {signInLabel}\n </NavigationLink>\n ) : null}\n </output>\n );\n}\n\n/**\n * Session storage, guarded at every access. A browser that refuses it — a shopper blocking site data —\n * throws rather than returning null, and a gate that cannot read its own record still has a shopper to\n * get to sign-in. A refusal therefore reads as \"no record\", which spends the hop; the cost is that the\n * loop protection is absent in that browser, which is the lesser of the two failures.\n */\nfunction readSignInAttempt(): string | null {\n try {\n return globalThis.sessionStorage.getItem(SIGN_IN_ATTEMPT_KEY);\n } catch {\n return null;\n }\n}\n\nfunction recordSignInAttempt(destination: string): void {\n try {\n globalThis.sessionStorage.setItem(SIGN_IN_ATTEMPT_KEY, destination);\n } catch {\n // Nothing to fall back to: the record is the only thing that outlives the hop.\n }\n}\n\nfunction clearSignInAttempt(): void {\n try {\n globalThis.sessionStorage.removeItem(SIGN_IN_ATTEMPT_KEY);\n } catch {\n // Nothing was written either, so there is nothing to undo.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAa,2BAA2B,kBAAkB;CACxD,IAAI;CACJ,OAAO;CACP,aAAa;CACb,QAAQ;EACN,MAAM;EACN,sBAAsB;EACtB,YAAY;GACV,cAAc;IAAE,MAAM;IAAU,WAAW;IAAG,OAAO;GAAuB;GAC5E,gBAAgB;IAAE,MAAM;IAAU,WAAW;IAAG,OAAO;GAAwB;GAC/E,yBAAyB;IAAE,MAAM;IAAU,WAAW;IAAG,OAAO;GAAmC;GACnG,aAAa;IAAE,MAAM;IAAU,WAAW;IAAG,OAAO;GAAqB;EAC3E;CACF;AACF,CAAC;AAsCD,MAAa,8BAA8B,gBAAwC;CACjF,MAAM;CACN,YAAY;CACZ,WAAW;CACX,SAAS;CACT,QAAQ;EAAE,OAAO;EAAuB,UAAU;CAAW;CAC7D,iBAAiB;CACjB,QAAQ;EAGN,cAAc;GAAE,MAAM;GAAU,OAAO;EAAuB;EAC9D,gBAAgB;GAAE,MAAM;GAAU,OAAO;EAAwB;EACjE,yBAAyB;GAAE,MAAM;GAAU,OAAO;EAAmC;EACrF,aAAa;GAAE,MAAM;GAAU,OAAO;EAAqB;CAC7D;CACA,WACE;AACJ,CAAC;;;AAID,MAAM,sBAAsB;AAS5B,SAAgB,kBAAkB,EAChC,cACA,gBACA,yBACA,aACA,UACA,YACmC;CACnC,MAAM,EAAE,OAAO,YAAY,WAAW;CACtC,MAAM,aAAa,iBAAiB;CACpC,MAAM,WAAW,YAAY;CAC7B,MAAM,SAAS,UAAU;CACzB,MAAM,aAAa,uBAAuB;CAI1C,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CAGpD,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CAIpD,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CAGtD,MAAM,oBAAoB,OAAO,CAAC;CAIlC,MAAM,CAAC,UAAU,eAAe,SAAyB,IAAI;CAI7D,MAAM,CAAC,QAAQ,aAAa,SAAwB,IAAI;CAKxD,gBAAgB;EACd,UAAU,WAAW,SAAS,MAAM;CACtC,CAAC;CAID,MAAM,aAAa,aAAa,OAAO,OAAO,GAAG,WAAW,UAAU;CAEtE,MAAM,UAAU,eAAe;EAAE;EAAY;EAAa;EAAc;EAAS;EAAO;EAAa;CAAS,CAAC;CAE/G,gBAAgB;EACd,QAAQ,SAAR;GACE,KAAK;IACH,eAAe,IAAI;IACnB,eAAe,KAAK;IACpB,mBAAmB;IACnB;GACF,KAAK;GACL,KAAK;IAGH,mBAAmB;IACnB;GACF,KAAK,cAAc;IAGjB,IAAI,aAAa,QAAQ,WAAW,MAClC;IAEF,IAAI,kBAAkB,MAAM,MAAM;KAChC,YAAY,IAAI;KAChB;IACF;IACA,MAAM,cAAc,kBAAkB,UAAU;IAChD,oBAAoB,WAAW;IAC/B,YAAY,KAAK;IAIjB,OAAO,QAAQ,WAAW;IAC1B;GACF;GACA,KAAK;GACL,KAAK,WACH;GACF,SACE,kBAAkB,OAAO;EAC7B;CACF,GAAG;EAAC;EAAS;EAAU;EAAQ;EAAY;CAAM,CAAC;CAElD,gBAAgB;EACd,SAAS,eAAe,OAAkC;GACxD,IAAI,CAAC,MAAM,WACT;GAKF,gBAAgB;IACd,gBAAgB,IAAI;GACtB,CAAC;EACH;EAEA,SAAS,eAAe,OAAkC;GAGxD,IAAI,CAAC,MAAM,WACT;GAEF,MAAM,aAAa,kBAAkB,UAAU;GAC/C,kBAAkB,UAAU;GAI5B,sBAAsB;GACtB,eAAe,KAAK;GACpB,gBAAgB,IAAI;GACpB,WAAgB,CAAC,CAAC,MAAM,WAAW;IAGjC,IAAI,eAAe,kBAAkB,SACnC;IAEF,gBAAgB,KAAK;IACrB,QAAQ,QAAR;KACE,KAAK;MACH,eAAe,IAAI;MACnB;KACF,KAAK;KACL,KAAK,eAGH;KACF,SACE,kBAAkB,MAAM;IAC5B;GACF,CAAC;EACH;EAEA,WAAW,iBAAiB,YAAY,cAAc;EACtD,WAAW,iBAAiB,YAAY,cAAc;EACtD,aAAa;GACX,WAAW,oBAAoB,YAAY,cAAc;GACzD,WAAW,oBAAoB,YAAY,cAAc;EAC3D;CACF,GAAG,CAAC,UAAU,CAAC;CAMf,gBAAgB,uBAAuB,CAAC,CAAC;CAGzC,MAAM,oBAAoB,UAAU,cAAc,KAAK,IAAI,SAAS,eAAe;CACnF,MAAM,sBAAsB,UAAU,gBAAgB,KAAK,IAAI,SAAS,iBAAiB;CACzF,MAAM,yBAAyB,UAAU,yBAAyB,KAAK,IACnE,SAAS,0BACT;CACJ,MAAM,mBAAmB,UAAU,aAAa,KAAK,IAAI,SAAS,cAAc;CAEhF,QAAQ,SAAR;EACE,KAAK;EACL,KAAK,cACH,OAAO;EACT,KAAK,WACH,OAGE,oBAAC,0BAAD;GAA0B,+BAA4B;GAAI,UAAA;EAA4C,CAAA;EAE1G,KAAK,WACH,OAAO,oBAAA,UAAA,EAAG,SAAW,CAAA;EACvB,KAAK;EACL,KAAK,sBACH,OACE,oBAAC,yBAAD;GACE,+BAA4B;GAC5B,QAAQ,YAAY,YAAY,YAAY;GAC5C,SAAS,YAAY,YAAY,sBAAsB;GACvD,YAAY,kBAAkB,UAAU;GACxC,aAAa;EACd,CAAA;EAEL,SACE,OAAO,kBAAkB,OAAO;CACpC;AACF;;;;;;;;;;;;AAaA,SAAS,eAAe,EACtB,YACA,aACA,cACA,SACA,OACA,aACA,YAS2B;CAC3B,IAAI,YACF,OAAO;CAKT,IAAI,aACF,OAAO,cAAc,YAAY;CAEnC,IAAI,gBAAgB,SAClB,OAAO;CAET,IAAI,UAAU,YACZ,OAAO;CAET,IAAI,aACF,OAAO;CAET,OAAO,aAAa,OAAO,uBAAuB;AACpD;AAEA,SAAS,kBAAkB,SAAuB;CAChD,MAAM,IAAI,MAAM,0CAA0C,OAAO,OAAO,GAAG;AAC7E;;;;;AAMA,SAAgB,yBAAyB,EAAE,WAAW,UAAU,GAAG,SAAwC;CACzG,OACE,oBAAC,UAAD;EAAQ,GAAI;EAAO,aAAU;EAAoB;EAAW,uCAAoC;EAC7F;CACK,CAAA;AAEZ;;;;;;;;;AAUA,SAAgB,wBAAwB,EACtC,QACA,SACA,YACA,aACA,WACA,GAAG,SAC4B;CAC/B,OACE,qBAAC,UAAD;EACE,GAAI;EACJ,aAAU;EACC;EACX,sCAAmC;EACnC,6CAA2C;EAL7C,UAAA,CAOG,UAAU,oBAAC,QAAD;GAAM,8CAA2C;GAAI,UAAA;EAAc,CAAA,IAAI,MACjF,aAAa,KAAK,IACjB,oBAAC,gBAAD;GACE,MAAM;GAGN,UAAU;GACV,6CAA0C;GAEzC,UAAA;EACa,CAAA,IACd,IACE;;AAEZ;;;;;;;AAQA,SAAS,oBAAmC;CAC1C,IAAI;EACF,OAAO,WAAW,eAAe,QAAQ,mBAAmB;CAC9D,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,oBAAoB,aAA2B;CACtD,IAAI;EACF,WAAW,eAAe,QAAQ,qBAAqB,WAAW;CACpE,QAAQ,CAER;AACF;AAEA,SAAS,qBAA2B;CAClC,IAAI;EACF,WAAW,eAAe,WAAW,mBAAmB;CAC1D,QAAQ,CAER;AACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/matter/account-access-gate/sign-out-intent.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Records that a sign-out is under way. Declared **before** the logout is awaited: the payload
|
|
4
|
+
* refreshes the session in place, and a gate that got its turn between the response landing and the
|
|
5
|
+
* navigation starting would redirect out from under the sign-out.
|
|
6
|
+
*/
|
|
7
|
+
export declare function declareSignOutIntent(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Takes the intent back. Called when the logout resolves with user errors — the shopper is still
|
|
10
|
+
* signed in and stays on the account page with the failure surfaced — and by the gate on a restored
|
|
11
|
+
* document, where the navigation the intent authorized has already happened.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withdrawSignOutIntent(): void;
|
|
14
|
+
/** Whether a sign-out is under way, re-rendering the caller when that changes. */
|
|
15
|
+
export declare function useSignOutIntent(): boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=sign-out-intent.d.ts.map
|