@wix/agent-skills 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +1 -1
- package/skills/README.md +23 -0
- package/skills/wix-app/SKILL.md +10 -2
- package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +326 -0
- package/skills/wix-app/references/CODE_QUALITY.md +1 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +124 -115
- package/skills/wix-app/references/DASHBOARD_PAGE.md +3 -0
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +23 -9
- package/skills/wix-app/references/SITE_PLUGIN.md +1 -1
- package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +115 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +264 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +37 -0
- package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +86 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +119 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +103 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +76 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +82 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +89 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +124 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +91 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +71 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +70 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +73 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +98 -0
- package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +278 -0
- package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +94 -0
- package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +43 -0
- package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +66 -0
- package/skills/wix-app/references/auto-patterns-dashboard/views.md +76 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -14
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +54 -29
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +2 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +17 -105
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +127 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +2 -1
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +15 -15
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +25 -53
- package/skills/wix-app/scripts/generate-auto-patterns.js +510 -0
- package/skills/wix-auth/SKILL.md +12 -0
- package/skills/wix-auth/references/device-flow.md +140 -0
- package/skills/wix-docs/SKILL.md +188 -0
- package/skills/wix-docs/references/API_SPEC_SEARCH.md +213 -0
- package/skills/wix-docs/references/EXTRACTING.md +120 -0
- package/skills/wix-headless/SKILL.md +84 -130
- package/skills/wix-headless/entry/bootstrap.mjs +93 -0
- package/skills/wix-headless/entry/skill.md +78 -0
- package/skills/wix-headless/references/AI_FEATURES.md +178 -0
- package/skills/wix-headless/references/CAPABILITIES.md +84 -0
- package/skills/wix-headless/references/DISCOVERY.md +34 -56
- package/skills/wix-headless/references/DOC_DISCOVERY.md +37 -0
- package/skills/wix-headless/references/FEEDBACK.md +118 -0
- package/skills/wix-headless/references/IMAGE_GENERATION.md +69 -0
- package/skills/wix-headless/references/SDK_HANDOFF.md +97 -0
- package/skills/wix-headless/references/SEED.md +44 -188
- package/skills/wix-headless/references/SETUP.md +32 -103
- package/skills/wix-headless/references/astro.md +98 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +196 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +236 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-bookings.md +279 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-cms.md +163 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-events.md +203 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +222 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +160 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +118 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-portfolio.md +132 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +188 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +173 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurants.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-blog.md +175 -0
- package/skills/wix-headless/references/inline-recipes/setup-bookings.md +228 -0
- package/skills/wix-headless/references/inline-recipes/setup-cms.md +200 -0
- package/skills/wix-headless/references/inline-recipes/setup-events.md +196 -0
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +174 -0
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +176 -0
- package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +153 -0
- package/skills/wix-headless/references/inline-recipes/setup-pricing-plans.md +229 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +117 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +152 -0
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +57 -0
- package/skills/wix-headless/references/managed/CONNECT.md +74 -0
- package/skills/wix-headless/references/managed/CREATE.md +130 -0
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +57 -0
- package/skills/wix-headless/references/non-astro.md +59 -0
- package/skills/wix-headless/references/self-managed/AUTHENTICATION.md +22 -0
- package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +7 -0
- package/skills/wix-headless/references/stripe/AUTHENTICATION.md +55 -0
- package/skills/wix-headless/references/stripe/DEPLOYMENT.md +63 -0
- package/skills/wix-manage/SKILL.md +24 -1
- package/skills/wix-manage/references/analytics/query-site-analytics.md +241 -0
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +1 -1
- package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +265 -0
- package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +436 -0
- package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +209 -0
- package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +249 -0
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +51 -0
- package/skills/wix-vibe-headless/SKILL.md +136 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +128 -0
- package/skills/wix-vibe-headless/references/blog/wix-blog.js +217 -0
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +153 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-checkout.js +147 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-services.js +215 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +154 -0
- package/skills/wix-vibe-headless/references/cms/wix-cms.js +186 -0
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +142 -0
- package/skills/wix-vibe-headless/references/events/wix-events-browse.js +135 -0
- package/skills/wix-vibe-headless/references/events/wix-events-registration.js +187 -0
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +211 -0
- package/skills/wix-vibe-headless/references/members/wix-members-auth.js +387 -0
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +111 -0
- package/skills/wix-vibe-headless/references/portfolio/wix-portfolio.js +194 -0
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +127 -0
- package/skills/wix-vibe-headless/references/pricing-plans/wix-pricing-plans.js +157 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +138 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-menu.js +224 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-ordering.js +156 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-reservations.js +107 -0
- package/skills/wix-vibe-headless/references/shared/wix-client.js +186 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +155 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-cart.js +147 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-catalog.js +142 -0
- package/skills/wix-headless/references/BUILD-astro.md +0 -253
- package/skills/wix-headless/references/BUILD-own-build.md +0 -167
- package/skills/wix-headless/references/BUILD.md +0 -105
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +0 -165
- package/skills/wix-headless/references/DISCOVERY-connect.md +0 -55
- package/skills/wix-headless/references/DISCOVERY-create.md +0 -238
- package/skills/wix-headless/references/PLAN-connect.md +0 -19
- package/skills/wix-headless/references/PLAN-create.md +0 -17
- package/skills/wix-headless/references/PLAN.md +0 -92
- package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +0 -300
- package/skills/wix-headless/references/astro/blog/BLOG_SETUP.md +0 -218
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +0 -45
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +0 -77
- package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +0 -214
- package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +0 -231
- package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +0 -453
- package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +0 -354
- package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +0 -299
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +0 -351
- package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +0 -119
- package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +0 -103
- package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +0 -132
- package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +0 -463
- package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +0 -119
- package/skills/wix-headless/references/astro/gift-cards/PAGES.md +0 -129
- package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +0 -178
- package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +0 -85
- package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +0 -129
- package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +0 -229
- package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +0 -215
- package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +0 -165
- package/skills/wix-headless/references/astro/templates/Layout.astro +0 -132
- package/skills/wix-headless/references/astro/templates/astro.config.mjs +0 -41
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +0 -374
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +0 -136
- package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +0 -25
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +0 -71
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +0 -54
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +0 -49
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +0 -245
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +0 -424
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +0 -176
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +0 -181
- package/skills/wix-headless/references/astro/templates/ecom/CartBadge.tsx +0 -84
- package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +0 -543
- package/skills/wix-headless/references/astro/templates/ecom/cart.astro +0 -13
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +0 -302
- package/skills/wix-headless/references/astro/templates/ecom/discounts.ts +0 -71
- package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +0 -24
- package/skills/wix-headless/references/astro/templates/gift-cards/GiftCardPurchase.tsx +0 -172
- package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +0 -42
- package/skills/wix-headless/references/astro/templates/gift-cards/_nav-snippet.astro +0 -11
- package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +0 -220
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +0 -51
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.ts +0 -75
- package/skills/wix-headless/references/astro/templates/global.css +0 -288
- package/skills/wix-headless/references/astro/templates/stores/AddToCartButton.tsx +0 -77
- package/skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx +0 -176
- package/skills/wix-headless/references/astro/templates/stores/CategoryRail.astro +0 -69
- package/skills/wix-headless/references/astro/templates/stores/ProductCard.astro +0 -79
- package/skills/wix-headless/references/astro/templates/stores/ProductPurchase.tsx +0 -397
- package/skills/wix-headless/references/astro/templates/stores/SeoTags.astro +0 -21
- package/skills/wix-headless/references/astro/templates/stores/back-in-stock.ts +0 -67
- package/skills/wix-headless/references/astro/templates/stores/categories.ts +0 -155
- package/skills/wix-headless/references/astro/templates/stores/category/[slug].astro +0 -130
- package/skills/wix-headless/references/astro/templates/stores/components-stores.css +0 -400
- package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +0 -128
- package/skills/wix-headless/references/astro/templates/stores/products/index.astro +0 -101
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +0 -161
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +0 -32
- package/skills/wix-headless/references/bookings/FLOW.md +0 -246
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +0 -95
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +0 -355
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +0 -176
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +0 -71
- package/skills/wix-headless/references/commands/known-apps.json +0 -6
- package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +0 -142
- package/skills/wix-headless/references/custom/INSTRUCTIONS.md +0 -88
- package/skills/wix-headless/references/custom/blog/WIRING.md +0 -52
- package/skills/wix-headless/references/custom/bookings/WIRING.md +0 -197
- package/skills/wix-headless/references/custom/cms/WIRING.md +0 -97
- package/skills/wix-headless/references/custom/ecom/WIRING.md +0 -67
- package/skills/wix-headless/references/custom/forms/WIRING.md +0 -101
- package/skills/wix-headless/references/custom/stores/WIRING.md +0 -80
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +0 -59
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +0 -43
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +0 -51
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +0 -357
- package/skills/wix-headless/references/seed-recipes.md +0 -49
- package/skills/wix-headless/references/shared/AGENTS_MD.md +0 -47
- package/skills/wix-headless/references/shared/AUTHENTICATION.md +0 -108
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +0 -8
- package/skills/wix-headless/references/shared/DESIGN_MD.md +0 -84
- package/skills/wix-headless/references/shared/DOCS_SEARCH.md +0 -41
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +0 -186
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +0 -206
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +0 -97
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +0 -143
- package/skills/wix-headless/references/shared/STYLING.md +0 -140
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +0 -126
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +0 -252
- package/skills/wix-headless/references/verticals/_schema.json +0 -74
- package/skills/wix-headless/references/verticals/_schema.md +0 -95
- package/skills/wix-headless/references/verticals/blog.md +0 -31
- package/skills/wix-headless/references/verticals/bookings.md +0 -41
- package/skills/wix-headless/references/verticals/cms.md +0 -30
- package/skills/wix-headless/references/verticals/ecom.md +0 -25
- package/skills/wix-headless/references/verticals/forms.md +0 -26
- package/skills/wix-headless/references/verticals/gift-cards.md +0 -24
- package/skills/wix-headless/references/verticals/stores.md +0 -33
- package/skills/wix-headless/scripts/check-manifest.mjs +0 -335
- package/skills/wix-headless/scripts/compose.mjs +0 -520
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -157
- package/skills/wix-headless/scripts/flatten.sh +0 -61
- package/skills/wix-headless/scripts/generate-images.mjs +0 -242
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +0 -154
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +0 -190
- package/skills/wix-headless/scripts/scaffold.sh +0 -139
- package/skills/wix-headless/scripts/seed-utilities.sh +0 -93
- package/skills/wix-headless/scripts/write-decorative-json.mjs +0 -62
- package/skills/wix-headless/shared-utilities/analytics.ts +0 -40
- package/skills/wix-headless/shared-utilities/ricos.ts +0 -179
- package/skills/wix-headless/shared-utilities/wix-image.ts +0 -72
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "How to Code Forms"
|
|
3
|
+
description: The frontend contract for Wix Forms — reading the live form schema (`@wix/forms` `forms.getForm`/`queryForms`/`listForms`, anonymous visitor, NO `auth.elevate`) to render fields schema-driven, then the `submissions.createSubmission(submission, options)` write path, keying the `submissions` map by each field's `target` (not label/id), why the anonymous visitor can submit with no captcha and no `auth.elevate`, why *submissions* read-back is owner-only, and the `_id` result field. Specifies the *how* (modules + exact calls + the failure modes the docs omit); which forms to render come from the request.
|
|
4
|
+
---
|
|
5
|
+
**RECIPE**: How to Code a Wix Forms Frontend (`@wix/forms`, Form Submissions v4)
|
|
6
|
+
|
|
7
|
+
A concise contract for writing the **frontend code** that renders a seeded form and submits it: **reading the live schema to build the inputs**, collecting the values, and creating a submission. **This recipe is the *how* (which modules, which calls), not the *what*** — which forms to render, how the page looks, and the framework are decided by the request you're fulfilling.
|
|
8
|
+
|
|
9
|
+
> **This recipe is for CODING the frontend, not for seeding it.** It assumes the form schema already exists (created by `setup-forms.md`) — you have each form's `formId` from the seed's `seeded.forms` map. **The field set, order, labels, `required`, validation format, and dropdown options are read live from the schema at render time** (see "Rendering" below) — the handoff carries only the `formId`. It says nothing about creating forms — only how to read, render, and submit them from frontend code.
|
|
10
|
+
|
|
11
|
+
> **Render schema-driven by default — read the schema and generate the inputs.** This is the standard path for **every** forms site and what this section documents: render inputs from the live form schema (field set, order, labels, `required`, format→type, length/pattern constraints, dropdown options) so an owner edit in the Wix dashboard reflects on the site with no code change, and so the submit map can never desync from the schema. **A form with a dropdown/enum field, or a site with more than one form, is NOT a reason to hardcode — it's the opposite:** dropdown options and per-field validation come *from* the schema, so hardcoding them is strictly more code and more drift. Each form simply reads its own schema by `formId`.
|
|
12
|
+
>
|
|
13
|
+
> **The ONLY exception is a brief that is explicitly design-led** — where the visual design is the whole point, the form is a small fixed part of a hand-crafted layout, and dashboard-editability is explicitly not a goal (e.g. the `inquiry-design` wedding-photographer brief). There you may hardcode `<input name="<target>">` from the known field set — the `target`s are still the contract (see "Submitting"). Even then, prefer schema-driven if the form has a dropdown or non-trivial validation. **If the brief doesn't call out design as the priority, render schema-driven.** "It has two forms" / "it has a dropdown" / "it's a simple contact form" are all still schema-driven.
|
|
14
|
+
|
|
15
|
+
> **⚠️ Reading rule — always append `?apiView=SDK` to every doc link below.** The Wix docs render two views of the same page. The **bare / REST view** shows the wrapped REST body (`{ submission: {…} }`) and an `id` field; the **`?apiView=SDK` view** shows the SDK call (`submissions.createSubmission(submission, options)`) and the normalized **`_id`** field. The SDK is what your frontend calls; reading the REST view leads to the wrong call signature and the `id`-vs-`_id` trap.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## The module and the client (read this first)
|
|
20
|
+
|
|
21
|
+
**⚠️ Two modules of `@wix/forms`, both used from the frontend:**
|
|
22
|
+
- **`forms`** — reads the form **schema** to render fields: `import { forms } from "@wix/forms"`, then `forms.getForm(formId)` / `forms.queryForms(query)` / `forms.listForms(namespace)`.
|
|
23
|
+
- **`submissions`** — writes a submission: `import { submissions } from "@wix/forms"`, then `submissions.createSubmission(submission, options)`.
|
|
24
|
+
|
|
25
|
+
(The frontend **reads** schemas and **writes** submissions; it never *creates* schemas — that's the seed.)
|
|
26
|
+
|
|
27
|
+
| Need | Package | Module / export |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Read a form schema (render its fields) | `@wix/forms` | `forms` |
|
|
30
|
+
| Submit a form (create a submission) | `@wix/forms` | `submissions` |
|
|
31
|
+
|
|
32
|
+
**⚠️ CRITICAL (Gate 0, verified live 2026-07-05): a plain anonymous visitor token reads the form schema — NO `auth.elevate`, no backend, no owner creds.** `forms.getForm(formId)` / `forms.listForms("wix.form_app.form")` return **200** with the full schema (all `formFields`, `fields[]` with `target`+`validation`+`view.label`, the `steps` layout, `submitSettings`) on the same visitor client that submits. **This contradicts the docs**: the API spec lists every read method (`getForm`/`queryForms`/`listForms`/`getFormSummary`) as requiring the owner scope `WIX_FORMS.FORM_SCHEMA_READ` ("Manage Submissions"), and every SDK example ships an "(with elevated permissions)" variant. **Trust the live 200, not the permission table** — Wix grants implicit visitor read for published-site rendering. **Do NOT add `auth.elevate` to the read** (v1's Astro path did, unnecessarily); a pure SPA/static frontend can't elevate anyway and doesn't need to. Schema-driven rendering is therefore **universal — every framework, including a pure static SPA, with no server or proxy.**
|
|
33
|
+
|
|
34
|
+
**Submissions read-back stays owner-only.** Only the *schema* read is visitor-accessible. **Reading submissions back** (`querySubmissionsByNamespace`, `getSubmission`, `countSubmission`) requires the owner permission `WIX_FORMS.SUBMISSION_READ_ANY` and is **not** available to a visitor — don't try to list/confirm submissions from the frontend. The submit's resolved promise **is** your success signal (show a thank-you); the lead lands in the site owner's dashboard.
|
|
35
|
+
|
|
36
|
+
**Auth / client — framework split (the SAME client both reads the schema and submits — no elevate on either, per Gate 0):**
|
|
37
|
+
- **Astro (Wix-managed):** authentication is ambient. Call `forms.getForm(...)` / `forms.listForms(...)` in the page frontmatter / a server route, and `submissions.createSubmission(...)` from a server route (`src/pages/api/*.ts`) or a server action — **no `createClient`, no `OAuthStrategy`, no `clientId`, no `auth.elevate`.** (The schema read can equally run client-side; server-side in SSR is simplest so the fields are in the initial HTML.)
|
|
38
|
+
- **Non-Astro (Vite/React/Vue/static):** build one manual visitor client with **both** modules and reuse it — the read runs **client-side**, no server needed:
|
|
39
|
+
```js
|
|
40
|
+
import { createClient, OAuthStrategy } from '@wix/sdk';
|
|
41
|
+
import { forms, submissions } from '@wix/forms';
|
|
42
|
+
|
|
43
|
+
const client = createClient({
|
|
44
|
+
modules: { forms, submissions },
|
|
45
|
+
auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
|
|
46
|
+
});
|
|
47
|
+
// read to render: client.forms.getForm(formId)
|
|
48
|
+
// then submit: client.submissions.createSubmission(submission)
|
|
49
|
+
```
|
|
50
|
+
The `clientId` is public, not a secret.
|
|
51
|
+
|
|
52
|
+
**⚠️ CRITICAL: do NOT call `auth.elevate` to submit.** An anonymous visitor **can** create a submission on the plain visitor token (it stamps `submitter.visitorId`). A pure SPA/static frontend has **no server and cannot elevate** anyway. (The Velo docs example wraps `createSubmission` in `auth.elevate` inside a backend `web.js` — that's the Velo hosted pattern, **not** the headless visitor path; ignore it here.)
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## The features (build the ones the site needs)
|
|
57
|
+
|
|
58
|
+
A forms frontend is essentially one feature — **render the fields, then submit** — plus the render/validation details around it. Implement it once per form the site shows.
|
|
59
|
+
|
|
60
|
+
### Rendering the inputs (read the schema, then bind `name` = `target`)
|
|
61
|
+
|
|
62
|
+
**Read the live schema, project its `fields` into a render model, and generate one input per field.** The field's **`target`** (the immutable snake_case key — `first_name`, `email`, `message`) becomes the input's **`name`** — it's the contract the submission must use. Because the inputs are generated from the schema, the field **set, order, labels, `required`, validation format, length/pattern constraints, and dropdown options** all track the dashboard: a field the owner adds/removes/relabels — or a constraint they tighten — reflects on the site with no code change.
|
|
63
|
+
|
|
64
|
+
**1 · Fetch the schema** (visitor token, no elevate — Gate 0). `getForm` resolves to the **`Form` directly** (not `{ form }`-wrapped). Fetch by id when you have it (the handoff carries `formId`), else discover by namespace:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { forms } from '@wix/forms'; // Astro: call directly. Non-Astro: client.forms
|
|
68
|
+
|
|
69
|
+
const form = await forms.getForm(SEEDED_FORM_ID); // from seeded.forms[].formId; returns the Form
|
|
70
|
+
// or discover: const { forms: list } = await forms.listForms('wix.form_app.form'); const form = list[0];
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**2 · Project `form.fields` into a render model — read `fields[]`, NOT `formFields[]`.** ⚠️ **This is the one shape trap that decides whether custom dropdowns render at all, and it has a typing wrinkle — read this whole paragraph.** The response carries **two** field arrays:
|
|
74
|
+
- **`formFields[]`** — the *dashboard-materialized* subset, and the only one in the SDK's typed `Form`. A field whose `identifier` isn't a recognized `CONTACTS_*` value is **dropped from `formFields[]`** server-side. **Every dropdown falls in this bucket** (dropdowns don't map to a `CONTACTS_*` system field — `identifier: "budget"`, `"subject"`, etc.), so `formFields[]` carries **no dropdown at all**, let alone its options. A `formFields[]`-based projection silently renders zero `<select>`s.
|
|
75
|
+
- **`fields[]`** — the *complete* normalized list: **every** field (contact-mapped *and* custom), each with `target`, `view.label`/`view.placeholder`/**`view.options`** (the dropdown choices, as `[{id,label,value}]`), and `validation`. This is the source you want. **Caveat:** `fields[]` is returned by the endpoint at runtime but is **not in the SDK's TypeScript `Form` type** (which types only `formFields`) — access it as `(form as any).fields` / cast it. **Verify during the live eval that the visitor SDK read actually surfaces `fields[].view.options`** (Gate 0 confirmed the visitor gets the full schema via REST; confirm the SDK wrapper preserves `fields`). If a particular SDK build strips it, read the schema with a **direct visitor-token `fetch`** to `GET https://www.wixapis.com/form-schema-service/v4/forms/{formId}` instead — same 200, same `fields[]`.
|
|
76
|
+
|
|
77
|
+
**Project from `fields[]`** (shape verified live 2026-07-12):
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const fields = ((form /* as any */).fields ?? []) // `fields` is runtime-present but untyped — cast in TS (see caveat above)
|
|
81
|
+
.filter((f) => f.target && !f.hidden) // INPUT fields carry a `target`; skip the submit button
|
|
82
|
+
.map((f) => ({
|
|
83
|
+
target: f.target, // → input `name`
|
|
84
|
+
label: f.view?.label ?? f.target,
|
|
85
|
+
placeholder: f.view?.placeholder ?? '',
|
|
86
|
+
required: f.validation?.required ?? false,
|
|
87
|
+
format: f.validation?.string?.format, // EMAIL | PHONE | URL | UNDEFINED | …
|
|
88
|
+
minLength: f.validation?.string?.minLength, // owner-set length/pattern constraints —
|
|
89
|
+
maxLength: f.validation?.string?.maxLength, // carry them so the client validates them too,
|
|
90
|
+
pattern: f.validation?.string?.pattern, // not just the server (see "client-side validation" below)
|
|
91
|
+
viewFieldType: f.view?.fieldType, // echoes the identifier: CONTACTS_*, TEXT_AREA, or a custom key
|
|
92
|
+
options: f.view?.options?.map((o) => ({ // present ⇒ this is a dropdown/enum field
|
|
93
|
+
value: o.value ?? o.label ?? '',
|
|
94
|
+
label: o.label ?? o.value ?? '',
|
|
95
|
+
})),
|
|
96
|
+
}));
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**3 · Render `fields.map(...)`**, deriving the control from the projected model — **a dropdown is signalled by `options`, a textarea by `viewFieldType`/`target`, the input `type` by `format`:**
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
fields.map((field) => {
|
|
103
|
+
// Dropdown → <select> — detected by the presence of `options` (works for custom dropdowns too)
|
|
104
|
+
if (field.options?.length) {
|
|
105
|
+
return (
|
|
106
|
+
<label key={field.target}>{field.label}{field.required && ' *'}
|
|
107
|
+
<select name={field.target} required={field.required} defaultValue="">
|
|
108
|
+
<option value="" disabled>{field.placeholder || 'Select…'}</option>
|
|
109
|
+
{field.options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
110
|
+
</select>
|
|
111
|
+
</label>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
// Textarea — detected by viewFieldType/target (see gotcha below), NEVER a componentType
|
|
115
|
+
if (field.viewFieldType === 'TEXT_AREA' || field.target === 'message') {
|
|
116
|
+
return (
|
|
117
|
+
<label key={field.target}>{field.label}{field.required && ' *'}
|
|
118
|
+
<textarea name={field.target} required={field.required} rows={4}
|
|
119
|
+
minLength={field.minLength} maxLength={field.maxLength} />
|
|
120
|
+
</label>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// Text input — derive `type` from the validation format; stamp the schema's constraints
|
|
124
|
+
const type =
|
|
125
|
+
field.format === 'EMAIL' ? 'email' :
|
|
126
|
+
field.format === 'PHONE' ? 'tel' :
|
|
127
|
+
field.format === 'URL' ? 'url' : 'text';
|
|
128
|
+
return (
|
|
129
|
+
<label key={field.target}>{field.label}{field.required && ' *'}
|
|
130
|
+
<input name={field.target} type={type} required={field.required} placeholder={field.placeholder}
|
|
131
|
+
minLength={field.minLength} maxLength={field.maxLength} pattern={field.pattern} />
|
|
132
|
+
</label>
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**⚠️ Detection signals — off `fields[].view`, never a `componentType`.**
|
|
138
|
+
- **Dropdown:** `field.view.options?.length` (a non-empty array). Custom dropdowns keep their options here even though they're absent from `formFields[]`. Don't look for a `"DROPDOWN"` componentType on `fields[]` — there isn't one.
|
|
139
|
+
- **Textarea:** `field.view.fieldType === "TEXT_AREA"` **or** `field.target === "message"`. The seed marks a textarea via `identifier: "TEXT_AREA"`, which reads back as `view.fieldType`; the underlying `componentType` is always `"TEXT_INPUT"` (there is no `"TEXT_AREA"` componentType). Keying off componentType would render every text field single-line.
|
|
140
|
+
- **Format:** `field.validation.string.format` reads back as `"UNDEFINED"` for unconstrained text (you seed `UNKNOWN_FORMAT`; it's stored as `UNDEFINED`) and `EMAIL`/`PHONE`/`URL`/… otherwise — map it to the input `type`.
|
|
141
|
+
|
|
142
|
+
### Validation — schema-driven on the client, authoritative on the server
|
|
143
|
+
|
|
144
|
+
The schema carries the validation rules too: `required`, the `format` (EMAIL/PHONE/URL), and `minLength`/`maxLength`/`pattern`. **Two layers, and the split matters:**
|
|
145
|
+
|
|
146
|
+
**Server (authoritative).** `createSubmission` enforces the field's `validation` block server-side. Any violation — missing required, bad email, too short/long, pattern mismatch — comes back as `err.details.validationError.fieldViolations[].data.errors[].errorPath` (= the field's `target`) with a human message (e.g. `"must have between 2 and 12 characters"`). You **must** map these back to per-input errors (see the mapping in "Submitting"); this is the backstop that always holds even if a rule is missed client-side.
|
|
147
|
+
|
|
148
|
+
**Client (schema-driven UX).** Pre-validate from the *same projected constraints* so the user gets inline feedback before a round-trip — and **derive every check from the schema, never from the field name.** ⚠️ The common mistake (seen in practice) is keying the email check on `target === "email"`; do it off `format` so an owner-added PHONE/URL/length rule is honored with no code change:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
function fieldError(field, raw) {
|
|
152
|
+
const v = (raw ?? '').trim();
|
|
153
|
+
if (field.required && !v) return `${field.label} is required.`;
|
|
154
|
+
if (!v) return ''; // optional + empty → ok
|
|
155
|
+
if (field.minLength && v.length < field.minLength) return `${field.label} must be at least ${field.minLength} characters.`;
|
|
156
|
+
if (field.maxLength && v.length > field.maxLength) return `${field.label} must be at most ${field.maxLength} characters.`;
|
|
157
|
+
if (field.format === 'EMAIL' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return 'Please enter a valid email address.';
|
|
158
|
+
if (field.format === 'URL' && !/^https?:\/\/.+/.test(v)) return 'Please enter a valid URL.';
|
|
159
|
+
if (field.format === 'PHONE' && !/^[+()\-\s\d]{7,}$/.test(v)) return 'Please enter a valid phone number.';
|
|
160
|
+
if (field.pattern && !new RegExp(field.pattern).test(v)) return `${field.label} is not in the expected format.`;
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Run it over the projected `fields` on submit (and optionally on blur); block the submit if any returns non-empty. The `<input>`/`<textarea>` also carry `minLength`/`maxLength`/`pattern` and (for text) the format-derived `type` from the render step, so native constraints reinforce the JS checks. **Don't hardcode a fixed field list or a per-field rule** — both the render and the validation read the live schema, so a dashboard-added constraint (a new `maxLength`, a stricter `pattern`) takes effect on the site with no code change.
|
|
166
|
+
|
|
167
|
+
**The `steps` layout is Phase-2 concern — ignore it for Phase 1.** The schema also carries a `steps` layout (rows/columns/multi-page); Phase 1 renders a flat, stacked list in `fields` order and the frontend owns its own visual layout. (Honoring `steps` for side-by-side / multi-page rendering is a separate, later capability.)
|
|
168
|
+
|
|
169
|
+
**Drift-safety — why schema-driven closes the desync.** Because every input is generated from the live schema, the submit map is built from the *same* `target`s the form actually declares, so an owner-side field add/remove/relabel in the dashboard **cannot** desync the frontend from the schema: a new field renders and submits, a removed field disappears, and there is no hand-maintained `target` list to drift out of sync. The "unseeded/misspelled target → `400 additional properties`" foot-gun (below) therefore disappears for owner edits — a mismatched key can now only come from the *seed* omitting a field's `validation` block, not from stale frontend markup.
|
|
170
|
+
|
|
171
|
+
### Submitting (create a submission)
|
|
172
|
+
|
|
173
|
+
Collect the input values into a **`submissions` object keyed by `target`**, and call `createSubmission`. Doc: <https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/create-submission?apiView=SDK>
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import { submissions } from '@wix/forms'; // Astro: call directly. Non-Astro: client.submissions
|
|
177
|
+
|
|
178
|
+
async function submitContact(formEl) {
|
|
179
|
+
const data = Object.fromEntries(new FormData(formEl)); // { first_name, email, message } — keys already = targets
|
|
180
|
+
const { _id } = await submissions.createSubmission({
|
|
181
|
+
formId: SEEDED_FORM_ID, // from seeded.forms[].formId
|
|
182
|
+
submissions: data, // map of target -> value
|
|
183
|
+
});
|
|
184
|
+
return _id; // success signal; then render a thank-you
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**⚠️ CRITICAL: `createSubmission` takes POSITIONAL args — `createSubmission(submission, options)`, NOT `createSubmission({ submission })`.** The first argument **is** the submission object (`{ formId, submissions }`) directly; the optional second argument is `options` (e.g. `captchaToken`). Wrapping it as `{ submission: {…} }` is the REST body shape and does not type-check against the SDK.
|
|
189
|
+
|
|
190
|
+
**⚠️ CRITICAL: the `submissions` map is keyed by each field's `target` — never by the label or the field id.** `{ submissions: { first_name: "Jane", email: "j@x.com" } }` — the keys must be the exact `target`s the schema declares. A key that isn't a schema target (a label like `"First name"`, a guessed key, a field GUID) is rejected with **`400 "must NOT have additional properties"`** (the whole submission fails, not just that field). This is why the render step binds **`name` = `target`** (from the projected schema) — a plain `FormData → Object.fromEntries` then yields the right keys for free.
|
|
191
|
+
|
|
192
|
+
**⚠️ CRITICAL: a submission of a field the seed did NOT give a validation block also 400s "additional properties" — that's a SEED bug, not a frontend bug.** Wix Forms only accepts submission keys for fields that were seeded **with a `stringOptions.validation` block** (`setup-forms.md` STEP 2). If a submit rejects a target you're sure you're spelling right (`{ additionalProperty: "message" }`), the field was seeded without a validation block — fix the **seed** (add the block), don't mangle the frontend key. Symmetrically, **omitting a `required` field** 400s with a validation error — send every required target.
|
|
193
|
+
|
|
194
|
+
**⚠️ The result id is `_id`, not `id`.** The SDK normalizes the created submission to `_id` (the REST view shows `id`). Read `result._id` if you need it; for lead capture you usually just need the promise to resolve, then show a thank-you. Do **not** call `confirmSubmission` — it's an owner-only management call and unnecessary for lead capture (the submission is recorded on create).
|
|
195
|
+
|
|
196
|
+
**⚠️ Per-field validation errors — map `fieldViolations[].data.errors[].errorPath` back to the input.** When a submit fails server-side validation (e.g. a malformed email), the rejection carries per-field detail at `err.details.validationError.fieldViolations[]`, each with a `data.errors[]` array whose `errorPath` is the field's `target` and `errorMessage` is the message. Project it into a `{ target → message }` map to show inline errors next to the right input, rather than a single form-level error:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const violations = err?.details?.validationError?.fieldViolations ?? [];
|
|
200
|
+
const fieldErrors = {};
|
|
201
|
+
for (const v of violations)
|
|
202
|
+
for (const fe of (v?.data?.errors ?? []))
|
|
203
|
+
if (fe.errorPath && !fieldErrors[fe.errorPath]) fieldErrors[fe.errorPath] = fe.errorMessage ?? 'Invalid value';
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Spam protection (only if the site raised it)
|
|
207
|
+
|
|
208
|
+
The seed leaves `spamFilterProtectionLevel` at its default (`ADVANCED`), and an anonymous visitor submit **still succeeds without a captcha token** on the headless SDK path. So you normally pass **no** `options`. Only if a site is configured to *require* a CAPTCHA do you need to solve one and pass it as the second arg: `createSubmission(submission, { captchaToken })`. Don't add captcha plumbing speculatively — the default path needs none.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Conclusion
|
|
213
|
+
A correct Wix Forms frontend:
|
|
214
|
+
- **reads the live schema** with the **`forms`** export of **`@wix/forms`** (`getForm`/`listForms`/`queryForms`) on the **plain visitor token — NO `auth.elevate`** (Gate 0: schema read is visitor-accessible on every framework incl. pure SPA, despite the docs marking it owner-scoped), and renders **schema-driven**: projects **`form.fields`** (the complete list — NOT `formFields`, which drops custom dropdowns) → `{ target, label, placeholder, required, format, minLength, maxLength, pattern, viewFieldType, options }` and generates one input per field so a dashboard field add/remove/relabel — or a changed constraint — reflects with no code change;
|
|
215
|
+
- derives the control from `fields[].view` — **`<select>`** when `view.options` is non-empty (works for custom dropdowns), **`<textarea>`** when `view.fieldType === "TEXT_AREA"` or `target === "message"` (**never** off a componentType), else an `<input>` whose `type` comes from `validation.string.format`, with `minLength`/`maxLength`/`pattern` stamped on;
|
|
216
|
+
- **validates schema-driven on the client and treats the server as authoritative**: pre-checks `required` + `minLength`/`maxLength`/`pattern` + format **keyed off `field.format`, never the field name**; and always maps `createSubmission`'s `fieldViolations[].data.errors[].errorPath` back to per-input errors as the backstop;
|
|
217
|
+
- imports the **`submissions`** export of **`@wix/forms`** and calls **`submissions.createSubmission(submission, options)`** — **positional args**, the submission object first (never `{ submission }`-wrapped);
|
|
218
|
+
- binds each input's **`name` = the schema's `target`** and builds the **`submissions` map keyed by `target`** (not label, not field id) — a wrong/unseeded key 400s the whole submission as "additional properties"; maps `fieldViolations[].data.errors[].errorPath` back to per-input errors;
|
|
219
|
+
- **only writes submissions** — reading *submissions* back is owner-only, so the resolved promise is the success signal (show a thank-you); never `confirmSubmission` (owner-only, unneeded);
|
|
220
|
+
- submits on the **plain visitor token with NO `auth.elevate`** (anonymous submit works; a pure SPA can't elevate anyway) and, at the default `ADVANCED` protection, **no captcha**;
|
|
221
|
+
- reads the created id as **`result._id`**, never `result.id`;
|
|
222
|
+
- treats a `400 "additional properties"` on a correctly-spelled target as a **seed** gap (the field lacks a `stringOptions.validation` block — fix `setup-forms.md`), not a frontend bug — and because inputs are schema-generated, this can no longer be caused by stale frontend markup.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "How to Code Members (Astro)"
|
|
3
|
+
description: The frontend contract for member sign-up / log-in / log-out and member-gated surfaces on a Wix-managed **Astro** frontend — the built-in `/api/auth/login` + `/api/auth/logout` routes that `@wix/astro` ships, the `returnUrl` param, gating a page by resolving the member in SSR / a backend route, reading the current member with `@wix/members` `getCurrentMember`, and the two rules that keep this path from breaking: **no `OAuthStrategy`/no client** (that 500s under auto-auth) and **no `auth.elevate`** for a member reading their own data. Specifies the *how* for the Astro axis only — read `how-to-code-members-non-astro.md` for any non-Astro frontend.
|
|
4
|
+
---
|
|
5
|
+
**RECIPE**: How to Code Member Auth on a Wix-managed **Astro** Frontend (built-in `/api/auth/*`, `@wix/members`)
|
|
6
|
+
|
|
7
|
+
A concise contract for wiring **login / sign-up / logout and member-gated surfaces** into an Astro frontend. **This is the *how* (which routes, which module, which failure modes), not the *what*** — which pages are gated, what the account page shows, and the design come from the request you're fulfilling.
|
|
8
|
+
|
|
9
|
+
> **⚠️ AXIS GUARD — this recipe is for Astro only.** On managed-Astro authentication is ambient and login ships as built-in routes. If the frontend is **not** Astro (Vite/React/Vue SPA, static HTML, self-managed), **stop and read `how-to-code-members-non-astro.md`** — that path builds a manual `OAuthStrategy` handshake, which is the opposite of what this recipe says. Cross-contaminating them is a real failure: building an `OAuthStrategy` client on Astro **500s at SSR** (`astro.md` — "no client" is the whole point of the integration).
|
|
10
|
+
|
|
11
|
+
> **One mechanism, not three.** Sign-up, log-in and log-out are the *same* flow. The Wix login page **logs in an existing member or registers a new one** in the same step; you never build a separate "sign up" call. Log-out is the inverse of the same flow.
|
|
12
|
+
|
|
13
|
+
> **Login surface — this recipe is the Wix-hosted login page (the default).** If the brief **explicitly** asks for a **custom/branded in-app login form or custom sign-up fields** (full name / username / address / arbitrary fields), that's the *custom login page* surface — **read `how-to-code-members-custom-login.md`**. Note custom login is a client-driven `OAuthStrategy` flow with **no client** under Astro auto-auth, so on Astro it means instantiating an explicit `OAuthStrategy` client in a backend route or a client island (never in SSR frontmatter) — take it on only on real intent; otherwise the built-in routes below are the default. The choice is **intent, not project type**.
|
|
14
|
+
|
|
15
|
+
Pinned docs (read before wiring — `curl` the `.md` directly):
|
|
16
|
+
- Member login on Astro: <https://dev.wix.com/docs/go-headless/wix-managed-headless/authentication/handle-member-login-using-wix-s-astro-integration.md>
|
|
17
|
+
- The integration keystone (auto-auth, "no client"): <https://dev.wix.com/docs/go-headless/wix-managed-headless/authentication/about-the-astro-integration.md>
|
|
18
|
+
- Current member (SDK): <https://dev.wix.com/docs/sdk/frontend-modules/members/current-member/introduction> (open with `?apiView=SDK`)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Identity vs. profile — two layers, don't conflate them (read this first)
|
|
23
|
+
|
|
24
|
+
The docs draw a hard line, and so must the code:
|
|
25
|
+
|
|
26
|
+
- **Identity / auth** — *"is this caller a logged-in member?"*, log in, log out. Native to the headless OAuth app. **No app install needed.** Everything in "Log in / sign up / log out" and "Gate a surface" below runs on this layer alone.
|
|
27
|
+
- **Member profile / Members Area** — reading the member's name / photo / roles / badges, an editable my-account page. Served by the **Wix Members Area app**, which **must be installed** on the site (see `SETUP.md`). `@wix/members` `getCurrentMember()` returns member data only once that app is present.
|
|
28
|
+
|
|
29
|
+
So a paywall that only needs "logged-in vs not" runs on identity alone; anything that **displays or edits member data** needs the Members Area app installed. If `getCurrentMember()` returns empty/errors on a site where login clearly worked, suspect the **Members Area app isn't installed** — not a code bug.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Log in / sign up / log out — built-in routes, zero wiring
|
|
34
|
+
|
|
35
|
+
`@wix/astro` ships the endpoints; you only render links/actions. **Do not build a login page or an OAuth handshake** — that's the non-Astro path.
|
|
36
|
+
|
|
37
|
+
```astro
|
|
38
|
+
<!-- log in OR sign up — same route, redirects to the Wix login page -->
|
|
39
|
+
<a href="/api/auth/login">Log in / Sign up</a>
|
|
40
|
+
|
|
41
|
+
<!-- land the member somewhere specific afterward -->
|
|
42
|
+
<a href="/api/auth/login?returnUrl=/account">Log in</a>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Logout is a **POST** to `/api/auth/logout` (optionally with `?returnUrl=`):
|
|
46
|
+
|
|
47
|
+
```astro
|
|
48
|
+
<form method="POST" action="/api/auth/logout"><button>Log out</button></form>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **`returnUrl` must be an allowed redirect URI.** When the project is created with that URL it's auto-added; an un-allow-listed `returnUrl` fails the redirect. Default to a relative path already known-good (the login page auto-allows the project's own origin paths).
|
|
52
|
+
- After login the member identity **rides on every subsequent SDK call automatically** — no client, no token handling. That's auto-auth (`astro.md`).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Gate a surface — resolve identity in SSR or a backend route
|
|
57
|
+
|
|
58
|
+
Gating is just *"do I have a member session?"*, decided **server-side** (SSR frontmatter or a `src/pages/api/*.ts` route), then bounce anonymous visitors to the built-in login route. This is the exact shape already used for blog comments (`SDK_HANDOFF.md` §5).
|
|
59
|
+
|
|
60
|
+
```astro
|
|
61
|
+
---
|
|
62
|
+
// src/pages/account.astro — gate the page in SSR frontmatter
|
|
63
|
+
import { members } from '@wix/members';
|
|
64
|
+
let me = null;
|
|
65
|
+
try {
|
|
66
|
+
const res = await members.getCurrentMember({ fieldsets: ['FULL'] });
|
|
67
|
+
me = res.member ?? null;
|
|
68
|
+
} catch { /* not a member / not installed — treat as anonymous */ }
|
|
69
|
+
|
|
70
|
+
if (!me) return Astro.redirect('/api/auth/login?returnUrl=/account');
|
|
71
|
+
---
|
|
72
|
+
<h1>Welcome, {me.profile?.nickname ?? me.loginEmail}</h1>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Guard the SSR call in `try/catch`** — an unguarded throw truncates the response mid-stream (white screen; `astro.md` A3). An anonymous visitor is a normal state, not an error: catch → treat as logged-out → redirect to login.
|
|
76
|
+
- **Don't render account/gated UI as a client island that checks auth in the browser.** Resolve identity server-side and gate the route; render only what a member should see. (Same reasoning as the blog-comment API-endpoint path — avoid the browser-auth detour.)
|
|
77
|
+
- For an **action** that only *some* callers may take (post a comment, place a member order), don't gate the whole page: render the control always, and on submit POST to a `src/pages/api/*.ts` endpoint that resolves the session and, if the caller isn't a member, redirects to `/api/auth/login?returnUrl=…`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Read the current member — `@wix/members`, not the dev-preview package
|
|
82
|
+
|
|
83
|
+
```astro
|
|
84
|
+
import { members } from '@wix/members';
|
|
85
|
+
const { member } = await members.getCurrentMember({ fieldsets: ['FULL'] });
|
|
86
|
+
// member.profile?.nickname, member.profile?.photo, member.loginEmail, member.contactId, member.roles
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **⚠️ The SDK export is `getCurrentMember`, NOT `getMyMember`.** The REST method is named *Get My Member* and the SDK docs page may show `GetMyMember`, but `@wix/members` exports it as **`members.getCurrentMember`** — calling `members.getMyMember(...)` throws `is not a function` at runtime. This bites because a logged-out smoke test never reaches the call; it only fails once a real member loads the page.
|
|
90
|
+
- **⚠️ Use `@wix/members` (`members.getCurrentMember` / `getMember` / `updateMember`).** It needs only **visitor/member auth** and is production-ready. **Do NOT reach for `@wix/site-members`** (`wixSiteMembers`, frontend "Current Member" `getMember`/`getRoles`/`makeProfilePublic`) — it's in **Developer Preview** ("not intended for production"). Use it only if a profile-privacy toggle is *specifically* requested.
|
|
91
|
+
- The member's **photo** field is a `wix:image://` identifier — resolve it with `media.getScaledToFillImageUrl` like any other Wix image (`astro.md` A6); never hand-build the CDN URL.
|
|
92
|
+
- **Reading another member by id returns only their PUBLIC fieldset**, and a **private** profile returns nothing to a visitor/member identity. This is why the blog-comment author lookup (`SDK_HANDOFF.md` §5) quietly assumes the author's profile is public — the same caveat applies to any "look up a member by id" here.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## ⚠️ Do NOT `auth.elevate()` for member features (the most likely wrong turn)
|
|
97
|
+
|
|
98
|
+
Member login and elevation are **different axes**:
|
|
99
|
+
|
|
100
|
+
- **Login** moves you along the **identity** axis: anonymous visitor → a specific member.
|
|
101
|
+
- **`auth.elevate()`** moves you along the **permission** axis: caller → *app/admin* scope.
|
|
102
|
+
|
|
103
|
+
A logged-in member reading **their own** data — own orders, own bookings, own subscriptions, plan-gated content, their profile — is authorized for it under the **member token, with no elevation**. Reaching for `auth.elevate()` to "make member data work" is the classic mistake: it's the wrong axis, and it doesn't grant a member access to their own data (they already have it).
|
|
104
|
+
|
|
105
|
+
`auth.elevate()` is only for **site-wide/admin** reads (everyone's orders, listing all members). On Astro that goes in a **backend route** (`src/pages/api/*.ts` wrapping `auth.elevate()`, `astro.md` §2) — never inline in a page. If you're gating content by the member's own plan, that's a member-token read; listing *all* plans/orders as admin is the elevate read. Keep them separate.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## pricing-plans is a HARD dependency on members
|
|
110
|
+
|
|
111
|
+
If the site has pricing-plans (membership / subscription / paid tiers), **login is required, not optional**. Ordering a plan (`orders.createOnlineOrder(planId)`) orders it **for a logged-in member**; if none is logged in the Wix flow forces sign-up. `orders.memberListOrders()` and "my subscription" reads return **nothing** for an anonymous visitor.
|
|
112
|
+
|
|
113
|
+
So: browsing the plans grid is public, but the **subscribe** button and the **my-subscription** surface both need the login mechanism above. A logged-in member calling `orders.createOnlineOrder` needs **no `onBehalf`** — the order is created on their behalf from the member session. (Everywhere else — stores "my orders", bookings "my bookings", events "my registrations" — member login is a *soft* add-on: the purchase/RSVP/book action itself runs fine as an anonymous visitor; only the account view of it needs a member.)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Conclusion
|
|
118
|
+
Correct member auth on an Astro frontend:
|
|
119
|
+
- uses the **built-in `/api/auth/login`** (login *and* sign-up) and **POST `/api/auth/logout`** routes with an allow-listed `returnUrl` — the login handshake **never** builds an `OAuthStrategy` client, and one must never be built in **SSR frontmatter** (the public-env `clientId` is `undefined` at server render → 500). (The one exception is the custom-login surface — `how-to-code-members-custom-login.md` — which builds an explicit client in a backend route or client island, never in SSR frontmatter.)
|
|
120
|
+
- gates surfaces **server-side** (SSR frontmatter or a `src/pages/api/*.ts` route), guarded in `try/catch`, bouncing anonymous visitors to `/api/auth/login?returnUrl=…`;
|
|
121
|
+
- reads the current member via **`@wix/members` `getCurrentMember`** (not the dev-preview `@wix/site-members`), resolving the photo `wix:image://` URI and expecting only PUBLIC fields for other members;
|
|
122
|
+
- does **no `auth.elevate`** for a member reading their own data (elevation is a different, admin-only axis that lives in a backend route);
|
|
123
|
+
- treats **login as required** whenever pricing-plans is present (subscribe + my-subscription), and as a soft add-on for the "my …" surfaces of the other verticals;
|
|
124
|
+
- needs the **Wix Members Area app installed** only for profile *data* (name/photo/roles) — pure "logged-in vs not" gating runs on the identity layer with no install.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "How to Code Members (custom login page)"
|
|
3
|
+
description: The frontend contract for a **custom, branded login/sign-up page** — you build the UI and drive Wix's auth service directly with the `OAuthStrategy` SDK (`register` / `login` / `processVerification` / `getMemberTokensForDirectLogin` / `setTokens` / `sendPasswordResetEmail` / `logout`), instead of redirecting to the Wix-hosted login page. This is the surface to use **only when the brief asks for a custom/in-app login form or custom sign-up fields** (name, address, username, arbitrary fields); otherwise use the Wix login page (`how-to-code-members-*.md`). Covers the `loginState` state machine (SUCCESS / EMAIL_VERIFICATION_REQUIRED / OWNER_APPROVAL_REQUIRED / FAILURE), custom `profile` fields at sign-up, token persistence, reading the member with `@wix/members`, and the no-`auth.elevate` rule. Works on **any** headless project (managed or self-managed) — the choice is intent, not project type.
|
|
4
|
+
---
|
|
5
|
+
**RECIPE**: How to Code a **Custom Login Page** (branded in-app auth, `OAuthStrategy` `register`/`login`, `@wix/members`)
|
|
6
|
+
|
|
7
|
+
A concise contract for building your **own** login / sign-up / logout UI and driving Wix's authentication service directly — instead of bouncing the visitor to the Wix-hosted login page. **This is the *how* (which SDK calls, which states, which failure modes), not the *what*** — the form design, which pages are gated, and what the account page shows come from the request you're fulfilling.
|
|
8
|
+
|
|
9
|
+
> **⚠️ WHEN TO USE THIS RECIPE — intent, not project type.** Reach for custom login **only when the brief explicitly asks for it**: a **branded / in-app login form** (the visitor never leaves your UI) *or* **custom sign-up fields** (full name, username/nickname, address, arbitrary fields). If the brief just says "let members log in / sign up / have an account" with no mention of a custom form or extra fields, **use the Wix login page instead** — `how-to-code-members-astro.md` (Astro) or `how-to-code-members-non-astro.md` (non-Astro). The Wix login page needs zero UI and is the deterministic default; custom login is more code and more failure modes, so take it on only on real intent.
|
|
10
|
+
|
|
11
|
+
> **Works on any headless project.** The Wix docs frame custom login pages as "self-managed only," but that's positioning, not a technical lockout — the `register`/`login` SDK methods (and their `Register V2`/`Login V2` IAM endpoints) authenticate against the same Wix auth service and **work on managed projects too**. So **do not branch on project type** — if the intent is custom login, build it; the flow below is identical on managed and self-managed.
|
|
12
|
+
|
|
13
|
+
> **One client, one flow — sign-up and log-in are the same machine.** Both `register()` and `login()` return the **same** `StateMachine` shape and both end at `getMemberTokensForDirectLogin` → `setTokens`. Member tokens are the **same shape** as visitor tokens (`role: member`); logging in just swaps the token set on the visitor client you already have.
|
|
14
|
+
|
|
15
|
+
> **⚠️ This is a client-driven (`OAuthStrategy`) flow — it is the non-Astro shape.** `register`/`login` live on `client.auth`, i.e. the manual visitor client `non-astro.md` builds. Astro's auto-auth ships **no client** to call them on. So: on a **non-Astro** frontend (SPA / static), this is the natural path. On **Astro**, custom login means stepping *outside* auto-auth — instantiate an explicit `OAuthStrategy` client (in a backend route or a client island) to run these calls; do **not** expect the built-in `/api/auth/*` routes to do custom login (they only drive the Wix login page). If the brief is Astro *and* doesn't demand a custom form, prefer the Wix login page (`how-to-code-members-astro.md`).
|
|
16
|
+
|
|
17
|
+
Pinned docs (read before wiring — `curl` the `.md` directly):
|
|
18
|
+
- Custom login via the JS SDK: <https://dev.wix.com/docs/go-headless/self-managed-headless/authentication/members/custom-login-page/custom-login/custom-login-using-the-js-sdk.md>
|
|
19
|
+
- `OAuthStrategy` reference (`register`/`login`/`processVerification`/`getMemberTokensForDirectLogin`/`sendPasswordResetEmail`, the `StateMachine` + `IdentityProfile` objects): <https://dev.wix.com/docs/sdk/core-modules/sdk/oauth-strategy.md>
|
|
20
|
+
- Current member (SDK): <https://dev.wix.com/docs/sdk/frontend-modules/members/current-member/introduction> (open with `?apiView=SDK`)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Preconditions
|
|
25
|
+
|
|
26
|
+
- **`clientId` only, never the secret.** Custom login uses the same **public** OAuth client id as the visitor client (`non-astro.md` N6). No `client_secret` in the frontend, ever.
|
|
27
|
+
- **No redirect / no allow-listed callback for login itself.** Unlike the Wix login page, `register`/`login` are **direct API calls** — the visitor never leaves your page, so there is **no `redirectUri` to allow-list** for the login round-trip. (Two side flows *do* redirect and need an allow-listed URI: `sendPasswordResetEmail(email, redirectUri)` and `logout(originalUrl)`'s `logoutUrl`. Allow-list those URLs on the OAuth app if you wire password reset / logout return.)
|
|
28
|
+
- **The visitor session is automatic.** The client mints an anonymous visitor token on first use, and `register`/`login` run under it — you do **not** hand-mint a visitor token first. (That manual step only exists in the raw-REST version of this flow.)
|
|
29
|
+
- **Profile data still needs the Members Area app.** Reading the member back (`getCurrentMember`) and **defining `customFields`** need the **Wix Members Area app** installed (`SETUP.md`) — see the identity-vs-profile split below.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Identity vs. profile — two layers, don't conflate them
|
|
34
|
+
|
|
35
|
+
- **Identity / auth** — sign up, log in, log out, "is this caller a member?". Native to the headless OAuth app. **No app install needed** — the whole `register`/`login`/`loggedIn()` flow below runs on this layer alone.
|
|
36
|
+
- **Member profile / Members Area** — reading name / photo / roles, an editable my-account page, **and any custom field definitions**. Served by the **Wix Members Area app**, which **must be installed** (`SETUP.md`). `@wix/members` `getCurrentMember()` returns data only once that app is present.
|
|
37
|
+
|
|
38
|
+
If `getCurrentMember()` is empty on a site where `loggedIn()` is `true`, suspect the **Members Area app isn't installed** — not a code bug.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## The flow — build a form, call the SDK, handle the state machine
|
|
43
|
+
|
|
44
|
+
Reuse the one visitor client `non-astro.md` builds (`createClient({ modules: { members }, auth: OAuthStrategy({ clientId }) })`) — don't make a second client.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// Sign up — register() takes a `profile` for custom fields (this is why you'd pick custom login)
|
|
48
|
+
const res = await client.auth.register({
|
|
49
|
+
email, password,
|
|
50
|
+
profile: { // all optional; include what the brief's sign-up form collects
|
|
51
|
+
firstName, lastName,
|
|
52
|
+
nickname, // ≈ username
|
|
53
|
+
addresses: [{ address: { /* … */ } }],
|
|
54
|
+
// customFields: { <fieldName>: <value> } // ⚠️ see note below — needs field defs
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Log in — same StateMachine shape, no profile
|
|
59
|
+
// const res = await client.auth.login({ email, password });
|
|
60
|
+
|
|
61
|
+
// Handle the result (both register and login return this)
|
|
62
|
+
switch (res.loginState) {
|
|
63
|
+
case 'SUCCESS': {
|
|
64
|
+
const tokens = await client.auth.getMemberTokensForDirectLogin(res.data.sessionToken);
|
|
65
|
+
client.auth.setTokens(tokens); // subsequent SDK calls now run as the member
|
|
66
|
+
persistTokens(tokens); // see persistence below
|
|
67
|
+
break; // now render the member UI / navigate
|
|
68
|
+
}
|
|
69
|
+
case 'EMAIL_VERIFICATION_REQUIRED': {
|
|
70
|
+
// Wix has emailed a 6-digit code. Collect it in your UI, then:
|
|
71
|
+
const v = await client.auth.processVerification({ verificationCode });
|
|
72
|
+
if (v.loginState === 'SUCCESS') {
|
|
73
|
+
const tokens = await client.auth.getMemberTokensForDirectLogin(v.data.sessionToken);
|
|
74
|
+
client.auth.setTokens(tokens); persistTokens(tokens);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'OWNER_APPROVAL_REQUIRED':
|
|
79
|
+
// Membership is pending owner approval — show a "pending" notice; they can log in once approved.
|
|
80
|
+
break;
|
|
81
|
+
case 'FAILURE':
|
|
82
|
+
// Surface a message keyed off res.errorCode:
|
|
83
|
+
// invalidEmail | invalidPassword | emailAlreadyExists | resetPassword
|
|
84
|
+
// | missingCaptchaToken | invalidCaptchaToken
|
|
85
|
+
// 'resetPassword' → call sendPasswordResetEmail (below), don't retry login.
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **`loggedIn()` gates everything.** `client.auth.loggedIn()` → `false` = anonymous, `true` = member. Render the account UI only when `true`; otherwise show your login form.
|
|
91
|
+
- **Logout** is a redirect (same as the Wix-login path): `const { logoutUrl } = await client.auth.logout(window.location.href); clearPersistedTokens(); window.location.href = logoutUrl;`
|
|
92
|
+
- **Password reset**: `await client.auth.sendPasswordResetEmail(email, redirectUri)` — `redirectUri` **must be an allow-listed** authorization redirect URI; Wix hosts the reset page and returns the member to it.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## The `loginState` state machine — handle every branch (don't assume SUCCESS)
|
|
97
|
+
|
|
98
|
+
`register()` / `login()` / `processVerification()` all return a `StateMachine` (`{ loginState, data: { sessionToken }, errorCode, error }`). The trap is coding only the happy path:
|
|
99
|
+
|
|
100
|
+
- **`SUCCESS`** — exchange `data.sessionToken` for tokens (above). Only here do you get a session.
|
|
101
|
+
- **`EMAIL_VERIFICATION_REQUIRED`** — fires when a brand-new registrant's email **already exists as a contact**, or when **email verification is enabled** in the site's Member Settings. You must build the code-entry step (`processVerification`) or login silently dead-ends. With default settings + a fresh email, you'll usually get `SUCCESS` straight away — but code the branch anyway; it's site-setting-dependent, not code-dependent.
|
|
102
|
+
- **`OWNER_APPROVAL_REQUIRED`** — signup policy is manual-approval; show "pending", don't treat as an error.
|
|
103
|
+
- **`FAILURE`** — branch on `errorCode` (`invalidEmail` / `invalidPassword` / `emailAlreadyExists` / `resetPassword` / captcha codes). `emailAlreadyExists` on register = route them to log in; `resetPassword` = send a reset email.
|
|
104
|
+
|
|
105
|
+
**Signup security (email verification, owner approval, reCAPTCHA) is dashboard-governed, not code.** These are Site-Member-Settings toggles — document them as host-configurable, don't try to set them from a headless run. There is **no headless TOTP/SMS 2FA**; the security layers are password + email verification + reCAPTCHA. If reCAPTCHA is enabled in settings, pass `captchaTokens` to `register`/`login` (`OAuthStrategy` exposes `captchaVisibleSiteKey`/`captchaInvisibleSiteKey`).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Custom sign-up fields — the whole reason to pick this surface
|
|
110
|
+
|
|
111
|
+
`register`'s `profile` (`IdentityProfile`) accepts `firstName`, `lastName`, `nickname` (≈ username), `picture`, `phones`, `addresses`, `labels`, `language`, `privacyStatus`, and **`customFields`** (an arbitrary `name → value` map). So "username + email + full name + address + password" maps straight onto `profile`.
|
|
112
|
+
|
|
113
|
+
- **⚠️ Standard fields work as-is; arbitrary `customFields` need field definitions.** `firstName`/`lastName`/`nickname`/`addresses`/`phones` are accepted directly. But keys inside `customFields` must first be **defined** via the Members Custom Fields API (which needs the **Members Area app**) — an undefined custom field is silently dropped. If the brief's extra fields are the standard ones, you need no field defs; only truly custom keys do.
|
|
114
|
+
- The Wix login page **cannot** collect any of these — that's the capability custom login uniquely unlocks, and the reason to take on the extra code.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Token persistence & renewal — treat member tokens like visitor tokens
|
|
119
|
+
|
|
120
|
+
Member tokens are the **same token set** the visitor session machinery already handles; login just swaps them in.
|
|
121
|
+
|
|
122
|
+
- **Persist** the token set (localStorage or a cookie) after `setTokens` so a reload stays logged in. On boot, if you have a stored set, `client.auth.setTokens(stored)` before first render — or pass `tokens` into `OAuthStrategy({ clientId, tokens })` at client creation.
|
|
123
|
+
- **Renew** with `client.auth.renewToken(refreshToken)` (or the SDK's automatic renewal when you re-hydrate via `setTokens`). A member session expiring is normal — refresh, don't force a re-login unless the refresh token is gone.
|
|
124
|
+
- On logout, **clear** the persisted set so the next boot is a clean anonymous visitor.
|
|
125
|
+
- **Keep one shared client instance** — creating a new client per component drops the session (a documented custom-login pitfall).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Read the current member — `@wix/members`, not the dev-preview package
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { members } from '@wix/members';
|
|
133
|
+
const { member } = await client.members.getCurrentMember({ fieldsets: ['FULL'] });
|
|
134
|
+
// member.profile?.nickname, member.profile?.photo, member.loginEmail, member.contactId, member.roles
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **⚠️ The SDK export is `getCurrentMember`, NOT `getMyMember`.** The REST method is named *Get My Member* and the SDK docs page may show `GetMyMember`, but `@wix/members` exports it as **`client.members.getCurrentMember`** — calling `getMyMember(...)` throws `is not a function` at runtime. Silent trap: a logged-out smoke test never reaches the call.
|
|
138
|
+
- **⚠️ Use `@wix/members`**, not the Developer-Preview `@wix/site-members`.
|
|
139
|
+
- The **photo** is a `wix:image://` identifier — resolve with `media.getScaledToFillImageUrl` (`non-astro.md` N7); never hand-build the CDN URL.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## ⚠️ Do NOT `auth.elevate()` — wrong axis (and unavailable on a SPA)
|
|
144
|
+
|
|
145
|
+
**Login** is the **identity** axis (anonymous → a specific member). **`auth.elevate()`** is the **permission** axis (→ app/admin scope). A member reading **their own** data (own profile, own orders/bookings/subscriptions, plan-gated or member-scoped content) is authorized under the **member token, no elevation** — reaching for elevate to "make member data work" is the wrong axis. A pure SPA has no server, so there is no `auth.elevate` at all (`non-astro.md` N5) — and member features don't need it.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Conclusion
|
|
150
|
+
Correct custom-login auth:
|
|
151
|
+
- is taken on **only when the brief asks for a branded/in-app login form or custom sign-up fields** — otherwise the Wix login page (`how-to-code-members-{astro,non-astro}.md`) is the default;
|
|
152
|
+
- works on **any** headless project (managed or self-managed) — **no project-type branch**;
|
|
153
|
+
- reuses the **one visitor `OAuthStrategy` client** and drives `register` / `login` / `processVerification` → `getMemberTokensForDirectLogin` → `setTokens`, with `logout` and `loggedIn()`;
|
|
154
|
+
- **handles the full `loginState` machine** (SUCCESS / EMAIL_VERIFICATION_REQUIRED / OWNER_APPROVAL_REQUIRED / FAILURE+`errorCode`), not just SUCCESS;
|
|
155
|
+
- collects custom fields via `register`'s `profile` — standard fields directly, arbitrary `customFields` only after defining them (needs the Members Area app);
|
|
156
|
+
- treats signup security (verification / approval / reCAPTCHA) as **dashboard-governed**, sets none from the run, and states MFA is unsupported headless;
|
|
157
|
+
- **persists and renews** member tokens like visitor tokens, on **one shared client**;
|
|
158
|
+
- reads the member via **`@wix/members` `getCurrentMember`** (not the dev-preview package);
|
|
159
|
+
- does **no `auth.elevate`** for own-data reads;
|
|
160
|
+
- needs the **Wix Members Area app** only for profile *data* and custom-field definitions — pure "logged-in vs not" gating runs on the identity layer with no install.
|