@rovela-ai/sdk 0.20.0 → 0.21.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/dist/admin/api/index.d.ts +2 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +2 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/integrations.d.ts +60 -0
- package/dist/admin/api/integrations.d.ts.map +1 -0
- package/dist/admin/api/integrations.js +337 -0
- package/dist/admin/api/integrations.js.map +1 -0
- package/dist/admin/components/AdminNav.d.ts.map +1 -1
- package/dist/admin/components/AdminNav.js +3 -1
- package/dist/admin/components/AdminNav.js.map +1 -1
- package/dist/admin/components/AdminSelect.d.ts.map +1 -1
- package/dist/admin/components/AdminSelect.js +2 -0
- package/dist/admin/components/AdminSelect.js.map +1 -1
- package/dist/admin/components/DiscountsManager.d.ts.map +1 -1
- package/dist/admin/components/DiscountsManager.js +2 -2
- package/dist/admin/components/DiscountsManager.js.map +1 -1
- package/dist/admin/components/IntegrationsManager.d.ts +5 -0
- package/dist/admin/components/IntegrationsManager.d.ts.map +1 -0
- package/dist/admin/components/IntegrationsManager.js +138 -0
- package/dist/admin/components/IntegrationsManager.js.map +1 -0
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +8 -4
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
- package/dist/admin/components/ShippingSettings.js +21 -21
- package/dist/admin/components/ShippingSettings.js.map +1 -1
- package/dist/admin/components/UsersTable.d.ts.map +1 -1
- package/dist/admin/components/UsersTable.js +2 -0
- package/dist/admin/components/UsersTable.js.map +1 -1
- package/dist/admin/components/VariantManager.d.ts.map +1 -1
- package/dist/admin/components/VariantManager.js +8 -2
- package/dist/admin/components/VariantManager.js.map +1 -1
- package/dist/admin/components/index.d.ts +2 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/integrations/brevo-logo.d.ts +11 -0
- package/dist/admin/integrations/brevo-logo.d.ts.map +1 -0
- package/dist/admin/integrations/brevo-logo.js +11 -0
- package/dist/admin/integrations/brevo-logo.js.map +1 -0
- package/dist/admin/integrations/registry.d.ts +116 -0
- package/dist/admin/integrations/registry.d.ts.map +1 -0
- package/dist/admin/integrations/registry.js +389 -0
- package/dist/admin/integrations/registry.js.map +1 -0
- package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
- package/dist/checkout/components/CheckoutFlow.js +0 -1
- package/dist/checkout/components/CheckoutFlow.js.map +1 -1
- package/dist/core/db/queries.d.ts +25 -25
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +5 -0
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/i18n/messages/en.d.ts.map +1 -1
- package/dist/core/i18n/messages/en.js +123 -0
- package/dist/core/i18n/messages/en.js.map +1 -1
- package/dist/core/i18n/messages/fr.d.ts.map +1 -1
- package/dist/core/i18n/messages/fr.js +123 -0
- package/dist/core/i18n/messages/fr.js.map +1 -1
- package/dist/products/api/products.d.ts +3 -1
- package/dist/products/api/products.d.ts.map +1 -1
- package/dist/products/api/products.js +29 -3
- package/dist/products/api/products.js.map +1 -1
- package/dist/products/components/VariantSelector.d.ts.map +1 -1
- package/dist/products/components/VariantSelector.js +4 -1
- package/dist/products/components/VariantSelector.js.map +1 -1
- package/dist/products/hooks/useProducts.d.ts +3 -1
- package/dist/products/hooks/useProducts.d.ts.map +1 -1
- package/dist/products/hooks/useProducts.js +4 -2
- package/dist/products/hooks/useProducts.js.map +1 -1
- package/docs/changelog/epoch-08-integrations.md +125 -0
- package/package.json +1 -1
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +19 -0
- package/templates/store-template/app/admin/(dashboard)/integrations/page.tsx +17 -0
- package/templates/store-template/app/api/admin/integrations/route.ts +9 -0
- package/templates/store-template/app/faq/page.tsx +42 -56
- package/templates/store-template/app/privacy/page.tsx +65 -105
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +6 -3
- package/templates/store-template/app/products/page.tsx +63 -1
- package/templates/store-template/app/terms/page.tsx +70 -198
- package/templates/store-template/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProducts.d.ts","sourceRoot":"","sources":["../../../src/products/hooks/useProducts.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAO/C,MAAM,WAAW,kBAAkB;IACjC,
|
|
1
|
+
{"version":3,"file":"useProducts.d.ts","sourceRoot":"","sources":["../../../src/products/hooks/useProducts.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAO/C,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iBAAiB;IACjB,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,MAAM,CAAA;IACrD,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACrC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,uBAAuB;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,SAAS,EAAE,OAAO,CAAA;IAClB,8BAA8B;IAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,4BAA4B;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAgI/E"}
|
|
@@ -35,7 +35,7 @@ import { useLocale } from '../../core/i18n/context';
|
|
|
35
35
|
*/
|
|
36
36
|
export function useProducts(options = {}) {
|
|
37
37
|
const activeLocale = useLocale();
|
|
38
|
-
const { category, search, sort = 'newest', attributes, minPrice, maxPrice, limit = 12, initialPage = 1, endpoint = '/api/products', } = options;
|
|
38
|
+
const { category, onSale, search, sort = 'newest', attributes, minPrice, maxPrice, limit = 12, initialPage = 1, endpoint = '/api/products', } = options;
|
|
39
39
|
const [products, setProducts] = useState([]);
|
|
40
40
|
const [total, setTotal] = useState(0);
|
|
41
41
|
const [page, setPage] = useState(initialPage);
|
|
@@ -53,6 +53,8 @@ export function useProducts(options = {}) {
|
|
|
53
53
|
params.set('locale', activeLocale);
|
|
54
54
|
if (category)
|
|
55
55
|
params.set('category', category);
|
|
56
|
+
if (onSale)
|
|
57
|
+
params.set('onSale', '1');
|
|
56
58
|
if (search)
|
|
57
59
|
params.set('search', search);
|
|
58
60
|
if (minPrice !== undefined)
|
|
@@ -63,7 +65,7 @@ export function useProducts(options = {}) {
|
|
|
63
65
|
params.set('attributes', JSON.stringify(attributes));
|
|
64
66
|
}
|
|
65
67
|
return params.toString();
|
|
66
|
-
}, [category, search, sort, attributes, minPrice, maxPrice, limit, activeLocale]);
|
|
68
|
+
}, [category, onSale, search, sort, attributes, minPrice, maxPrice, limit, activeLocale]);
|
|
67
69
|
// Fetch products
|
|
68
70
|
const fetchProducts = useCallback(async (pageNum, append = false) => {
|
|
69
71
|
setIsLoading(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProducts.js","sourceRoot":"","sources":["../../../src/products/hooks/useProducts.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"useProducts.js","sourceRoot":"","sources":["../../../src/products/hooks/useProducts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# E8 Integrations hub — 0.21.0..0.21.0
|
|
2
|
+
|
|
3
|
+
## What landed
|
|
4
|
+
The admin gained `/admin/integrations` — ONE surface where the merchant
|
|
5
|
+
manages credentials for every external service connected to the store
|
|
6
|
+
(Brevo, PayPal, Turnstile, GA4, Google Ads, Meta, TikTok, Klaviyo, Intercom,
|
|
7
|
+
Trustpilot, plus chat-installed custom integrations). Before this, rotating
|
|
8
|
+
a key meant re-running a Feature Store card (a paid AI turn) or a per-store
|
|
9
|
+
custom settings page; now it is a 20-second admin edit. Installing or
|
|
10
|
+
removing an integration stays a Features Store / chat action — this page
|
|
11
|
+
manages what is installed, it is not a catalog.
|
|
12
|
+
|
|
13
|
+
## SDK-internal (free via pin bump)
|
|
14
|
+
- `IntegrationsManager` (exported from `@rovela-ai/sdk/admin`) — the page
|
|
15
|
+
component; renders its own `AdminPageHeader` (bare-mount page wrapper).
|
|
16
|
+
- `admin/integrations/registry.ts` — the static registry of known providers.
|
|
17
|
+
Each entry names its storage: dedicated `store_settings` columns for
|
|
18
|
+
secrets (brevo_*, paypal_*, turnstile_*) or a slot in the `analytics`
|
|
19
|
+
JSONB (ga4, google_ads, meta, tiktok, klaviyo, intercom, trustpilot).
|
|
20
|
+
These storage locations are a CONTRACT with the platform's Feature Store
|
|
21
|
+
card prompts — never rename one side alone.
|
|
22
|
+
- Presence semantics: an entry renders only when its storage is detectable
|
|
23
|
+
on the store (column exists / JSONB key present) — a store never sees
|
|
24
|
+
integrations it does not have.
|
|
25
|
+
- Secrets: GET returns `{ hasValue, masked }` only; PUT is write-only
|
|
26
|
+
(blank keeps the stored value). Brevo (`GET /v3/account`) and PayPal
|
|
27
|
+
(OAuth token grant) are validated server-side BEFORE persisting — a dead
|
|
28
|
+
key is never saved.
|
|
29
|
+
- AdminNav Configuration group gained the `integrations` item (after Tax).
|
|
30
|
+
|
|
31
|
+
## Template contract
|
|
32
|
+
- `app/admin/(dashboard)/integrations/page.tsx` (NEW) — bare mount of
|
|
33
|
+
`IntegrationsManager`.
|
|
34
|
+
- `app/api/admin/integrations/route.ts` (NEW) — re-export
|
|
35
|
+
`getIntegrations as GET, saveIntegration as PUT` from
|
|
36
|
+
`@rovela-ai/sdk/admin/api`. Without it the page shows a permanent load
|
|
37
|
+
error.
|
|
38
|
+
|
|
39
|
+
## Behavior contracts
|
|
40
|
+
- NEW self-healed column `store_settings.custom_integrations jsonb NOT NULL
|
|
41
|
+
DEFAULT '[]'` — descriptor registry for integrations the SDK does not
|
|
42
|
+
know. Deliberately NOT in the drizzle schema (raw-SQL only, like
|
|
43
|
+
`analytics`), so `findSettings` full-row selects never depend on it; the
|
|
44
|
+
integrations API self-heals it at its own chokepoint. Descriptors carry
|
|
45
|
+
`{key, label, fields:[{id = store_settings column name, label, secret}]}`;
|
|
46
|
+
values ALWAYS live in dedicated store_settings columns (merchant-secret
|
|
47
|
+
rule). Malformed descriptors are skipped, never rendered.
|
|
48
|
+
- Custom-integration writes validate every field id against live
|
|
49
|
+
`information_schema` before a quoted-identifier UPDATE.
|
|
50
|
+
- Store-side Claude Code contract (sandbox CLAUDE.md §7.6 + build-feature
|
|
51
|
+
skill): NEVER build a settings page / credentials form / nav entry for
|
|
52
|
+
integration keys — persist to canonical storage and (for novel
|
|
53
|
+
integrations) register a descriptor so this page renders the form.
|
|
54
|
+
|
|
55
|
+
## Custom-code hazards (audit greps for CC)
|
|
56
|
+
- Grep `app/admin` for hand-built integration settings pages (e.g. a
|
|
57
|
+
`marketing/page.tsx` managing an API key) — they are superseded by
|
|
58
|
+
`/admin/integrations` when their storage matches a registry entry
|
|
59
|
+
(brevo_api_key/brevo_list_id/brevo_enabled for Brevo). Presentational-only
|
|
60
|
+
duplicates should be REMOVED (page + its API route + its `navItems`
|
|
61
|
+
entry); keep any business logic that is not credential management.
|
|
62
|
+
- Grep `navItems` in `app/admin/(dashboard)/layout.tsx` for entries whose
|
|
63
|
+
page only managed credentials — drop them alongside the page.
|
|
64
|
+
|
|
65
|
+
## MERCHANT-DATA HAZARD
|
|
66
|
+
None — no existing data changes meaning. The page is a view/editor over
|
|
67
|
+
storage that already exists; runtime readers are untouched.
|
|
68
|
+
|
|
69
|
+
## Also in 0.21.0 — storefront + admin correctness batch
|
|
70
|
+
|
|
71
|
+
SDK-internal (free via pin bump):
|
|
72
|
+
- Products API `?category=` accepts a SLUG as well as a UUID (store-authored
|
|
73
|
+
mega-menu links carry slugs; pre-0.21 a slug silently matched nothing and
|
|
74
|
+
the link showed the FULL catalog). Unknown slug → honest empty result.
|
|
75
|
+
- Products API + `useProducts` gained `onSale` (comparePrice > price), with
|
|
76
|
+
`countProducts` pagination parity — no more raw-SQL sale pages.
|
|
77
|
+
- `VariantSelector` renders attribute values in FIRST-SEEN order, not
|
|
78
|
+
alphabetical (sizes read S / M / L again, not L / M / S / XL).
|
|
79
|
+
- `VariantManager` sync compares variant combinations as KEY SETS, not
|
|
80
|
+
index-by-index — an order-only mismatch used to fire a normalizing
|
|
81
|
+
onChange right after ProductForm captured its dirty baseline, raising a
|
|
82
|
+
phantom "Unsaved changes" save bar on EVERY variant product open.
|
|
83
|
+
- Discounts page empty state got the full first-run treatment (icon +
|
|
84
|
+
title); `compact` stays for its nested sub-panels only.
|
|
85
|
+
- Admin conventions pass (shipping + integrations + order details):
|
|
86
|
+
Connected states are `admin-badge-success` pills (bare green text + icon
|
|
87
|
+
medallions removed), `admin-radio`/`admin-helper-text`/
|
|
88
|
+
`admin-label-required` adopted, `admin-spinner` replaces the animated
|
|
89
|
+
RefreshCw, sentence-case sweep, 16px icons, responsive form grids,
|
|
90
|
+
aria-labels on icon buttons, native select → AdminSelect and the
|
|
91
|
+
signature checkbox → AdminSwitch on the order ship card.
|
|
92
|
+
- i18n catalogs gained `faqPage.q/a.*`, `terms.*`, `privacy.*`,
|
|
93
|
+
`legal.*`, and `listing.{category,onSale,clearFilter}` keys (en + fr).
|
|
94
|
+
|
|
95
|
+
Template contract (new stores + SDK-update flow):
|
|
96
|
+
- `app/products/page.tsx` now READS `?category=` and `?sale=1` from the URL
|
|
97
|
+
(with the required Suspense boundary around `useSearchParams`) and shows
|
|
98
|
+
an active-filter chip. This is the page half of the category-links fix.
|
|
99
|
+
- `app/products/[slug]/ProductDetailClient.tsx` coerces prices to numbers
|
|
100
|
+
before the strikethrough comparison ('185.00' > '55.50' was
|
|
101
|
+
lexicographically false — the compare-at price never showed).
|
|
102
|
+
- `app/{faq,terms,privacy}/page.tsx` body copy now renders from the i18n
|
|
103
|
+
catalogs (was English literals even on French-default stores); FAQ
|
|
104
|
+
country names/lists localize via Intl.DisplayNames/ListFormat.
|
|
105
|
+
|
|
106
|
+
## Custom-code hazards (0.21.0 batch — audit greps for CC)
|
|
107
|
+
- Grep custom pages/sections for hand-rolled category or sale filtering
|
|
108
|
+
(raw SQL, client-side filters) — `/api/products?category=<slug>` and
|
|
109
|
+
`?onSale=1` now do it natively.
|
|
110
|
+
- Grep for links to `/products?category=` — they WORK now; stores that
|
|
111
|
+
worked around the bug with custom category pages can keep or migrate them.
|
|
112
|
+
|
|
113
|
+
## Verify
|
|
114
|
+
- `/admin/integrations` renders under Configuration in the admin nav.
|
|
115
|
+
- A store with Brevo columns shows the Brevo card; entering a bad key
|
|
116
|
+
surfaces Brevo's rejection inline and persists nothing.
|
|
117
|
+
- A saved secret displays as "ends in …xxxx"; saving with the secret blank
|
|
118
|
+
keeps the stored value.
|
|
119
|
+
- `SELECT custom_integrations FROM store_settings` returns `[]` (or the
|
|
120
|
+
registered descriptors) — never an error.
|
|
121
|
+
- `/products?category=<existing slug>` shows only that category;
|
|
122
|
+
`/products?sale=1` shows only discounted products (chip + clear visible).
|
|
123
|
+
- A product with comparePrice > price shows the strikethrough on its PDP.
|
|
124
|
+
- Opening a variant product in the admin does NOT raise the save bar.
|
|
125
|
+
- `/faq`, `/terms`, `/privacy` render in French on a fr-default store.
|
package/package.json
CHANGED
|
@@ -191,6 +191,25 @@ Active state is auto-detected from the URL — no extra config needed.
|
|
|
191
191
|
| Admin API auth check with `getServerSession` | Admin pages are already protected by `AdminGuard` layout |
|
|
192
192
|
| Forgot admin sidebar item | If feature has admin page, MUST add `navItems` to AdminLayout in layout.tsx |
|
|
193
193
|
|
|
194
|
+
## External-service credentials (integrations)
|
|
195
|
+
|
|
196
|
+
If the feature connects to an external service (API key, secret, tracking ID):
|
|
197
|
+
|
|
198
|
+
- Persist credentials to `store_settings` — dedicated columns for secrets (`ALTER TABLE store_settings ADD COLUMN IF NOT EXISTS …`, read server-side at request time; NEVER `.env.local`, NEVER `NEXT_PUBLIC_`), or the `analytics` JSONB for public tracking IDs.
|
|
199
|
+
- The merchant manages them at `/admin/integrations` (SDK page, already mounted). **Do NOT build a settings page, credentials form, or nav entry for keys.**
|
|
200
|
+
- Known keys are auto-rendered (brevo, paypal, turnstile, ga4, google_ads, meta, tiktok, klaviyo, intercom, trustpilot). A NOVEL integration registers a descriptor (dedup by key, then append):
|
|
201
|
+
|
|
202
|
+
```sql
|
|
203
|
+
ALTER TABLE store_settings ADD COLUMN IF NOT EXISTS custom_integrations jsonb NOT NULL DEFAULT '[]'::jsonb;
|
|
204
|
+
UPDATE store_settings SET custom_integrations =
|
|
205
|
+
(SELECT COALESCE(jsonb_agg(e), '[]'::jsonb)
|
|
206
|
+
FROM jsonb_array_elements(custom_integrations) e
|
|
207
|
+
WHERE e->>'key' <> '<key>')
|
|
208
|
+
|| jsonb_build_array('{"key":"<key>","label":"<Label>","fields":[{"id":"<store_settings_column>","label":"API key","secret":true,"required":true}]}'::jsonb);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Field `id`s = the store_settings column names (ALTERed first); mark secrets `"secret": true`. Tell the merchant credentials live under **Configuration → Integrations**.
|
|
212
|
+
|
|
194
213
|
## Checklist
|
|
195
214
|
|
|
196
215
|
- [ ] Tables created and verified with `list_objects`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Integrations Page
|
|
5
|
+
*
|
|
6
|
+
* Credentials for the external services connected to this store (Brevo,
|
|
7
|
+
* PayPal, analytics pixels, chat-installed custom integrations…). The
|
|
8
|
+
* IntegrationsManager renders the page header itself — this wrapper stays a
|
|
9
|
+
* bare mount. Installing or removing an integration is a Features Store /
|
|
10
|
+
* chat action on the Rovela platform; this page manages what is installed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { IntegrationsManager } from '@rovela-ai/sdk/admin'
|
|
14
|
+
|
|
15
|
+
export default function AdminIntegrationsPage() {
|
|
16
|
+
return <IntegrationsManager />
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Integrations API Route
|
|
3
|
+
*
|
|
4
|
+
* GET /api/admin/integrations - Installed integrations with masked secrets
|
|
5
|
+
* PUT /api/admin/integrations - Save one integration's credentials/settings
|
|
6
|
+
* (provider-validated before persisting)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { getIntegrations as GET, saveIntegration as PUT } from '@rovela-ai/sdk/admin/api'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
|
|
2
1
|
import type { Metadata } from 'next'
|
|
3
|
-
import {
|
|
2
|
+
import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
|
|
3
|
+
import { LocaleLink as Link, getT, intlLocaleFor } from '@rovela-ai/sdk/core'
|
|
4
4
|
import { findSettings } from '@rovela-ai/sdk/core/server'
|
|
5
5
|
import { buildFaqJsonLd, serializeJsonLd } from '@rovela-ai/sdk/core'
|
|
6
6
|
import { storeConfig } from '@/lib/store-config'
|
|
@@ -19,9 +19,14 @@ import {
|
|
|
19
19
|
*
|
|
20
20
|
* Server Component (no 'use client') so the schema + content are in the initial
|
|
21
21
|
* HTML — AI crawlers don't execute JavaScript.
|
|
22
|
+
*
|
|
23
|
+
* Every Q&A body comes from the i18n catalogs (they were English literals
|
|
24
|
+
* pre-0.21, which shipped an English FAQ on French-default stores). Country
|
|
25
|
+
* names + list joining use Intl.DisplayNames / Intl.ListFormat so they
|
|
26
|
+
* localize without a hand-maintained map.
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
|
-
export async function generateMetadata() {
|
|
29
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
25
30
|
const { locale } = await getRequestLocale()
|
|
26
31
|
const t = getT(locale)
|
|
27
32
|
return {
|
|
@@ -31,30 +36,22 @@ export async function generateMetadata() {
|
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
IT: 'Italy',
|
|
45
|
-
NL: 'the Netherlands',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatCountries(codes: string[]): string {
|
|
49
|
-
const names = codes.map((c) => COUNTRY_NAMES[c] || c)
|
|
50
|
-
if (names.length === 1) return names[0]
|
|
51
|
-
if (names.length === 2) return `${names[0]} and ${names[1]}`
|
|
52
|
-
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`
|
|
39
|
+
function formatCountries(codes: string[], intlLocale: string): string {
|
|
40
|
+
const display = new Intl.DisplayNames([intlLocale], { type: 'region' })
|
|
41
|
+
const names = codes.map((c) => {
|
|
42
|
+
try {
|
|
43
|
+
return display.of(c) || c
|
|
44
|
+
} catch {
|
|
45
|
+
return c
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
return new Intl.ListFormat(intlLocale, { style: 'long', type: 'conjunction' }).format(names)
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export default async function FaqPage() {
|
|
56
52
|
const { locale } = await getRequestLocale()
|
|
57
53
|
const t = getT(locale)
|
|
54
|
+
const intlLocale = intlLocaleFor(locale)
|
|
58
55
|
const settings = await findSettings().catch(() => null)
|
|
59
56
|
|
|
60
57
|
const countries = shipsToCountries(settings)
|
|
@@ -67,49 +64,38 @@ export default async function FaqPage() {
|
|
|
67
64
|
|
|
68
65
|
const faqs: Array<{ question: string; answer: string }> = [
|
|
69
66
|
{
|
|
70
|
-
question: '
|
|
67
|
+
question: t('faqPage.q.payment'),
|
|
68
|
+
answer: t('faqPage.a.payment'),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
question: t('faqPage.q.countries'),
|
|
72
|
+
answer: t('faqPage.a.countries', { countries: formatCountries(countries, intlLocale) }),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: t('faqPage.q.shippingCost'),
|
|
71
76
|
answer:
|
|
72
|
-
|
|
77
|
+
freeThreshold > 0
|
|
78
|
+
? t('faqPage.a.shippingCostFree', { threshold: freeThreshold, currency })
|
|
79
|
+
: t('faqPage.a.shippingCost'),
|
|
73
80
|
},
|
|
74
81
|
{
|
|
75
|
-
question: '
|
|
76
|
-
answer:
|
|
82
|
+
question: t('faqPage.q.delivery'),
|
|
83
|
+
answer: t('faqPage.a.delivery', { processing }),
|
|
77
84
|
},
|
|
78
|
-
freeThreshold > 0
|
|
79
|
-
? {
|
|
80
|
-
question: 'How much does shipping cost?',
|
|
81
|
-
answer: `Shipping is calculated at checkout based on your address. Orders over ${freeThreshold} ${currency} qualify for free shipping.`,
|
|
82
|
-
}
|
|
83
|
-
: {
|
|
84
|
-
question: 'How much does shipping cost?',
|
|
85
|
-
answer:
|
|
86
|
-
'Shipping is calculated at checkout based on your delivery address and the items in your order.',
|
|
87
|
-
},
|
|
88
85
|
{
|
|
89
|
-
question: '
|
|
90
|
-
answer:
|
|
86
|
+
question: t('faqPage.q.returns'),
|
|
87
|
+
answer:
|
|
88
|
+
returnDays > 0
|
|
89
|
+
? t('faqPage.a.returns', { days: returnDays })
|
|
90
|
+
: t('faqPage.a.noReturns'),
|
|
91
91
|
},
|
|
92
|
-
returnDays > 0
|
|
93
|
-
? {
|
|
94
|
-
question: 'What is your return policy?',
|
|
95
|
-
answer: `You can return unused items in their original condition within ${returnDays} days of delivery. Return shipping is the customer's responsibility unless the item arrived damaged or incorrect.`,
|
|
96
|
-
}
|
|
97
|
-
: {
|
|
98
|
-
question: 'What is your return policy?',
|
|
99
|
-
answer:
|
|
100
|
-
'All sales are final. If your item arrives damaged or incorrect, contact us right away and we will make it right.',
|
|
101
|
-
},
|
|
102
92
|
{
|
|
103
|
-
question: '
|
|
104
|
-
answer: guestCheckout
|
|
105
|
-
? 'No account is required — you can check out as a guest. Creating an account lets you track orders and check out faster next time.'
|
|
106
|
-
: 'Please create an account at checkout so you can track your order and manage returns.',
|
|
93
|
+
question: t('faqPage.q.account'),
|
|
94
|
+
answer: guestCheckout ? t('faqPage.a.accountGuest') : t('faqPage.a.accountRequired'),
|
|
107
95
|
},
|
|
108
96
|
{
|
|
109
|
-
question: '
|
|
110
|
-
answer: email
|
|
111
|
-
? `Email us at ${email} and we'll get back to you as soon as we can. You can also use our contact page.`
|
|
112
|
-
: `Reach us through our contact page and we'll get back to you as soon as we can.`,
|
|
97
|
+
question: t('faqPage.q.contact'),
|
|
98
|
+
answer: email ? t('faqPage.a.contactEmail', { email }) : t('faqPage.a.contactPage'),
|
|
113
99
|
},
|
|
114
100
|
]
|
|
115
101
|
|