@ecomiq/storefront 2.0.12 → 2.0.15
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/bin/ecomiq.mjs +3 -1
- package/package.json +4 -2
- package/src/auth/create-storefront-auth.ts +65 -6
- package/src/auth/ecomiq-oauth-password-constants.ts +1 -3
- package/src/auth/ecomiq-oauth-password-plugin.ts +12 -17
- package/src/auth/ecomiq-storefront-sign-in.ts +0 -3
- package/src/auth/secure-cookies.ts +23 -0
- package/src/auth/storefront-auth-base-url.ts +117 -0
- package/src/codegen.mjs +27 -1
- package/src/components/account/customer-addresses-panel.tsx +678 -0
- package/src/components/account/customer-order-detail-panel.tsx +285 -0
- package/src/components/account/customer-orders-panel.tsx +192 -0
- package/src/components/account/customer-profile-panel.tsx +379 -0
- package/src/components/checkout/checkout-contact-section.tsx +17 -11
- package/src/components/checkout/checkout-session-view.tsx +83 -2
- package/src/components/checkout/checkout-shipping-address-section.tsx +107 -2
- package/src/components/checkout/checkout-steps.ts +15 -0
- package/src/components/checkout/map-customer-to-checkout.ts +188 -0
- package/src/components/checkout/use-checkout-customer-prefill.ts +409 -0
- package/src/components/checkout/use-checkout-session.ts +5 -4
- package/src/config.d.ts +4 -0
- package/src/config.mjs +14 -0
- package/src/index.ts +54 -0
- package/src/pages/account/account-page-skeleton.tsx +69 -0
- package/src/pages/account/account-page-view.tsx +270 -0
- package/src/pages/account/account-shell.tsx +101 -0
- package/src/pages/account/parse-account-route.ts +36 -0
- package/src/pages/account.tsx +16 -0
- package/src/pages/checkout.$checkoutId.tsx +1 -1
- package/src/providers/auth.tsx +11 -2
- package/src/proxy-api.ts +20 -0
- package/src/vite-inline.mjs +54 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from 'react'
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
4
|
+
import {
|
|
5
|
+
createCustomerAddress,
|
|
6
|
+
customerMeToProfile,
|
|
7
|
+
deleteCustomerAddress,
|
|
8
|
+
getCustomerAddresses,
|
|
9
|
+
getCustomerMe,
|
|
10
|
+
getCustomerOrder,
|
|
11
|
+
getCustomerOrders,
|
|
12
|
+
patchCustomer,
|
|
13
|
+
toCustomerSavedAddresses,
|
|
14
|
+
updateCustomerAddress,
|
|
15
|
+
type CustomerAddressUpsertInput,
|
|
16
|
+
type CustomerProfilePatch,
|
|
17
|
+
} from '@ecomiq/core'
|
|
18
|
+
import { MapPinHouse, MonitorSmartphone, Package, UserRound } from 'lucide-react'
|
|
19
|
+
import { CustomerAddressesPanel } from '../../components/account/customer-addresses-panel'
|
|
20
|
+
import { CustomerOrderDetailPanel } from '../../components/account/customer-order-detail-panel'
|
|
21
|
+
import { CustomerOrdersPanel } from '../../components/account/customer-orders-panel'
|
|
22
|
+
import { CustomerProfilePanel } from '../../components/account/customer-profile-panel'
|
|
23
|
+
import {
|
|
24
|
+
useApiQuery,
|
|
25
|
+
useAuth,
|
|
26
|
+
useSearchNavigate,
|
|
27
|
+
useSettings,
|
|
28
|
+
} from '../../providers'
|
|
29
|
+
import { createStorefrontProxyApi } from '../../proxy-api'
|
|
30
|
+
import { AccountShell } from './account-shell'
|
|
31
|
+
import {
|
|
32
|
+
AccountPageSkeleton,
|
|
33
|
+
AccountSectionSkeleton,
|
|
34
|
+
} from './account-page-skeleton'
|
|
35
|
+
import { parseAccountRoute } from './parse-account-route'
|
|
36
|
+
|
|
37
|
+
const ACCOUNT_BASE = '/account'
|
|
38
|
+
const ORDERS_BASE = `${ACCOUNT_BASE}/orders`
|
|
39
|
+
const LOGIN_HREF = '/login'
|
|
40
|
+
const HOME_HREF = '/'
|
|
41
|
+
|
|
42
|
+
function proxyApi() {
|
|
43
|
+
return createStorefrontProxyApi()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function AccountPageView() {
|
|
47
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
48
|
+
const { section, orderId } = parseAccountRoute(pathname || ACCOUNT_BASE)
|
|
49
|
+
const navigate = useSearchNavigate()
|
|
50
|
+
const queryClient = useQueryClient()
|
|
51
|
+
const settings = useSettings()
|
|
52
|
+
const { client, session, isPending, signOut } = useAuth()
|
|
53
|
+
const isAuthenticated = Boolean(session?.user)
|
|
54
|
+
const redirectTargetRef = useRef<string | null>(null)
|
|
55
|
+
const language = settings.store.language || 'es-PE'
|
|
56
|
+
|
|
57
|
+
const profileQuery = useApiQuery(
|
|
58
|
+
['account-customer-me'],
|
|
59
|
+
async () => customerMeToProfile(await getCustomerMe(proxyApi())),
|
|
60
|
+
{
|
|
61
|
+
enabled: section === 'personal' && isAuthenticated && !isPending,
|
|
62
|
+
freshForMs: 60_000,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const addressesQuery = useApiQuery(
|
|
67
|
+
['customer-addresses'],
|
|
68
|
+
async () =>
|
|
69
|
+
toCustomerSavedAddresses(await getCustomerAddresses(proxyApi())),
|
|
70
|
+
{
|
|
71
|
+
enabled: section === 'addresses' && isAuthenticated && !isPending,
|
|
72
|
+
freshForMs: 30_000,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const ordersQuery = useApiQuery(
|
|
77
|
+
['customer-orders'],
|
|
78
|
+
async () => getCustomerOrders(proxyApi()),
|
|
79
|
+
{
|
|
80
|
+
enabled:
|
|
81
|
+
section === 'orders' && !orderId && isAuthenticated && !isPending,
|
|
82
|
+
freshForMs: 30_000,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const orderDetailQuery = useApiQuery(
|
|
87
|
+
['customer-order', orderId],
|
|
88
|
+
async () => getCustomerOrder(proxyApi(), orderId!),
|
|
89
|
+
{
|
|
90
|
+
enabled:
|
|
91
|
+
section === 'orders' && Boolean(orderId) && isAuthenticated && !isPending,
|
|
92
|
+
freshForMs: 30_000,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (isPending || isAuthenticated) return
|
|
98
|
+
|
|
99
|
+
const redirectTarget = redirectTargetRef.current ?? (pathname || ACCOUNT_BASE)
|
|
100
|
+
redirectTargetRef.current = redirectTarget
|
|
101
|
+
|
|
102
|
+
void navigate({
|
|
103
|
+
to: LOGIN_HREF,
|
|
104
|
+
search: { redirect: redirectTarget },
|
|
105
|
+
replace: true,
|
|
106
|
+
})
|
|
107
|
+
}, [isAuthenticated, isPending, navigate, pathname])
|
|
108
|
+
|
|
109
|
+
const handleLogout = async () => {
|
|
110
|
+
await signOut()
|
|
111
|
+
void navigate({ to: HOME_HREF, replace: true })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const invalidateAddresses = () =>
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: ['customer-addresses'] })
|
|
116
|
+
|
|
117
|
+
const handleUpdateProfile = async (payload: CustomerProfilePatch) => {
|
|
118
|
+
await patchCustomer(proxyApi(), payload)
|
|
119
|
+
const displayName = `${payload.firstName} ${payload.lastName}`.trim()
|
|
120
|
+
try {
|
|
121
|
+
await client.$fetch('/update-user', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: {
|
|
124
|
+
name: displayName || payload.firstName,
|
|
125
|
+
firstName: payload.firstName,
|
|
126
|
+
lastName: payload.lastName,
|
|
127
|
+
phone: payload.phone,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
} catch {
|
|
131
|
+
// La sesión Better Auth es secundaria; el PATCH upstream ya se aplicó.
|
|
132
|
+
}
|
|
133
|
+
await queryClient.invalidateQueries({ queryKey: ['account-customer-me'] })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const handleCreateAddress = async (payload: CustomerAddressUpsertInput) => {
|
|
137
|
+
await createCustomerAddress(proxyApi(), payload)
|
|
138
|
+
await invalidateAddresses()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleUpdateAddress = async (
|
|
142
|
+
addressId: string,
|
|
143
|
+
payload: CustomerAddressUpsertInput,
|
|
144
|
+
) => {
|
|
145
|
+
await updateCustomerAddress(proxyApi(), addressId, payload)
|
|
146
|
+
await invalidateAddresses()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const handleDeleteAddress = async (addressId: string) => {
|
|
150
|
+
await deleteCustomerAddress(proxyApi(), addressId)
|
|
151
|
+
await invalidateAddresses()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const menuItems = [
|
|
155
|
+
{
|
|
156
|
+
key: 'personal',
|
|
157
|
+
href: ACCOUNT_BASE,
|
|
158
|
+
label: 'Datos personales',
|
|
159
|
+
icon: <UserRound className="size-4" />,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: 'addresses',
|
|
163
|
+
href: `${ACCOUNT_BASE}/addresses`,
|
|
164
|
+
label: 'Direcciones',
|
|
165
|
+
icon: <MapPinHouse className="size-4" />,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: 'orders',
|
|
169
|
+
href: ORDERS_BASE,
|
|
170
|
+
label: 'Mis pedidos',
|
|
171
|
+
icon: <Package className="size-4" />,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'devices',
|
|
175
|
+
href: `${ACCOUNT_BASE}/devices`,
|
|
176
|
+
label: 'Dispositivos vinculados',
|
|
177
|
+
icon: <MonitorSmartphone className="size-4" />,
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if (isPending || !isAuthenticated) {
|
|
182
|
+
return <AccountPageSkeleton />
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let sectionContent: ReactNode
|
|
186
|
+
|
|
187
|
+
switch (section) {
|
|
188
|
+
case 'personal':
|
|
189
|
+
sectionContent = (
|
|
190
|
+
<SectionPanel>
|
|
191
|
+
<CustomerProfilePanel
|
|
192
|
+
profile={profileQuery.data}
|
|
193
|
+
isLoading={profileQuery.isLoading}
|
|
194
|
+
errorMessage={profileQuery.error?.message}
|
|
195
|
+
onUpdateProfile={handleUpdateProfile}
|
|
196
|
+
/>
|
|
197
|
+
</SectionPanel>
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
case 'addresses':
|
|
201
|
+
sectionContent = (
|
|
202
|
+
<SectionPanel>
|
|
203
|
+
<CustomerAddressesPanel
|
|
204
|
+
addresses={addressesQuery.data}
|
|
205
|
+
isLoading={addressesQuery.isLoading}
|
|
206
|
+
errorMessage={addressesQuery.error?.message}
|
|
207
|
+
onCreateAddress={handleCreateAddress}
|
|
208
|
+
onUpdateAddress={handleUpdateAddress}
|
|
209
|
+
onDeleteAddress={handleDeleteAddress}
|
|
210
|
+
/>
|
|
211
|
+
</SectionPanel>
|
|
212
|
+
)
|
|
213
|
+
break
|
|
214
|
+
case 'orders':
|
|
215
|
+
sectionContent = (
|
|
216
|
+
<SectionPanel>
|
|
217
|
+
{orderId ? (
|
|
218
|
+
<CustomerOrderDetailPanel
|
|
219
|
+
order={orderDetailQuery.data}
|
|
220
|
+
isLoading={orderDetailQuery.isLoading}
|
|
221
|
+
errorMessage={orderDetailQuery.error?.message}
|
|
222
|
+
backHref={ORDERS_BASE}
|
|
223
|
+
language={language}
|
|
224
|
+
productHref={(handle) => `/products/${encodeURIComponent(handle)}`}
|
|
225
|
+
/>
|
|
226
|
+
) : (
|
|
227
|
+
<CustomerOrdersPanel
|
|
228
|
+
orders={ordersQuery.data}
|
|
229
|
+
isLoading={ordersQuery.isLoading}
|
|
230
|
+
errorMessage={ordersQuery.error?.message}
|
|
231
|
+
orderDetailBasePath={ORDERS_BASE}
|
|
232
|
+
shopHref={HOME_HREF}
|
|
233
|
+
language={language}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
</SectionPanel>
|
|
237
|
+
)
|
|
238
|
+
break
|
|
239
|
+
case 'devices':
|
|
240
|
+
sectionContent = (
|
|
241
|
+
<SectionPanel>
|
|
242
|
+
<h2 className="text-lg font-medium text-foreground">Dispositivos vinculados</h2>
|
|
243
|
+
<p className="mt-2 text-muted-foreground">
|
|
244
|
+
Aún no hay endpoint para listar sesiones/dispositivos.
|
|
245
|
+
</p>
|
|
246
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
247
|
+
Cuando esté disponible el backend, podrás cerrar sesiones remotas desde aquí.
|
|
248
|
+
</p>
|
|
249
|
+
</SectionPanel>
|
|
250
|
+
)
|
|
251
|
+
break
|
|
252
|
+
default:
|
|
253
|
+
sectionContent = <AccountSectionSkeleton />
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<AccountShell
|
|
258
|
+
menuItems={menuItems}
|
|
259
|
+
onLogout={() => void handleLogout()}
|
|
260
|
+
pathname={pathname}
|
|
261
|
+
homeHref={HOME_HREF}
|
|
262
|
+
>
|
|
263
|
+
{sectionContent}
|
|
264
|
+
</AccountShell>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function SectionPanel({ children }: { children: ReactNode }) {
|
|
269
|
+
return <div className="px-6">{children}</div>
|
|
270
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { ArrowLeft, ChevronRight, LogOut } from 'lucide-react'
|
|
3
|
+
import { Link } from '../../link'
|
|
4
|
+
import { cn } from '../../ui/cn'
|
|
5
|
+
import {
|
|
6
|
+
accountSectionLabel,
|
|
7
|
+
parseAccountRoute,
|
|
8
|
+
} from './parse-account-route'
|
|
9
|
+
|
|
10
|
+
export type AccountMenuItem = {
|
|
11
|
+
key: string
|
|
12
|
+
href: string
|
|
13
|
+
label: string
|
|
14
|
+
icon: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AccountShellProps = {
|
|
18
|
+
menuItems: AccountMenuItem[]
|
|
19
|
+
onLogout: () => void
|
|
20
|
+
profileSyncError?: string | null
|
|
21
|
+
children: ReactNode
|
|
22
|
+
pathname: string
|
|
23
|
+
homeHref?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AccountShell({
|
|
27
|
+
menuItems,
|
|
28
|
+
onLogout,
|
|
29
|
+
profileSyncError,
|
|
30
|
+
children,
|
|
31
|
+
pathname,
|
|
32
|
+
homeHref = '/',
|
|
33
|
+
}: AccountShellProps) {
|
|
34
|
+
const { section, orderId } = parseAccountRoute(pathname || '/account')
|
|
35
|
+
const sectionTitle = accountSectionLabel(section, orderId)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="container max-w-6xl mx-auto px-4 py-8">
|
|
39
|
+
<Link
|
|
40
|
+
to={homeHref}
|
|
41
|
+
className="inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors [@media(hover:hover)]:hover:text-foreground"
|
|
42
|
+
>
|
|
43
|
+
<ArrowLeft className="size-4" />
|
|
44
|
+
<span className="underline">Volver a la tienda</span>
|
|
45
|
+
</Link>
|
|
46
|
+
|
|
47
|
+
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Mi perfil</h1>
|
|
48
|
+
|
|
49
|
+
{profileSyncError ? (
|
|
50
|
+
<p className="mt-3 text-sm text-amber-800">{profileSyncError}</p>
|
|
51
|
+
) : null}
|
|
52
|
+
|
|
53
|
+
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]">
|
|
54
|
+
<aside className="overflow-hidden rounded-xl border border-border bg-background lg:sticky lg:top-15 lg:self-start">
|
|
55
|
+
<nav className="divide-y divide-border">
|
|
56
|
+
{menuItems.map((item) => {
|
|
57
|
+
const isOrders = item.key === 'orders'
|
|
58
|
+
const active =
|
|
59
|
+
(section === item.key && !orderId) ||
|
|
60
|
+
(isOrders && section === 'orders' && Boolean(orderId))
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Link
|
|
64
|
+
key={item.key}
|
|
65
|
+
to={item.href}
|
|
66
|
+
className={cn(
|
|
67
|
+
'flex items-center justify-between border-b border-border px-4 py-4 text-[15px] transition-colors',
|
|
68
|
+
active
|
|
69
|
+
? 'bg-muted font-semibold'
|
|
70
|
+
: '[@media(hover:hover)]:hover:bg-muted',
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span className="flex items-center gap-3">
|
|
74
|
+
<span className="text-muted-foreground">{item.icon}</span>
|
|
75
|
+
<span>{item.label}</span>
|
|
76
|
+
</span>
|
|
77
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
78
|
+
</Link>
|
|
79
|
+
)
|
|
80
|
+
})}
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={onLogout}
|
|
84
|
+
className="flex w-full items-center justify-between px-4 py-4 text-left text-[15px] transition-colors [@media(hover:hover)]:hover:bg-muted"
|
|
85
|
+
>
|
|
86
|
+
<span className="flex items-center gap-3">
|
|
87
|
+
<LogOut className="size-4 text-muted-foreground" />
|
|
88
|
+
<span>Cerrar sesión</span>
|
|
89
|
+
</span>
|
|
90
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
91
|
+
</button>
|
|
92
|
+
</nav>
|
|
93
|
+
</aside>
|
|
94
|
+
|
|
95
|
+
<section className="min-w-0" aria-label={sectionTitle}>
|
|
96
|
+
{children}
|
|
97
|
+
</section>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AccountSection = 'personal' | 'addresses' | 'orders' | 'devices'
|
|
2
|
+
|
|
3
|
+
export type ParsedAccountRoute = {
|
|
4
|
+
section: AccountSection
|
|
5
|
+
orderId?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SECTION_LABELS: Record<AccountSection, string> = {
|
|
9
|
+
personal: 'Datos personales',
|
|
10
|
+
addresses: 'Direcciones',
|
|
11
|
+
orders: 'Mis pedidos',
|
|
12
|
+
devices: 'Dispositivos vinculados',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function accountSectionLabel(section: AccountSection, orderId?: string): string {
|
|
16
|
+
if (section === 'orders' && orderId) return 'Detalle del pedido'
|
|
17
|
+
return SECTION_LABELS[section]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Deriva sección y orderId desde `/account/...`. */
|
|
21
|
+
export function parseAccountRoute(pathname: string): ParsedAccountRoute {
|
|
22
|
+
const normalized = pathname.replace(/\/$/, '') || '/'
|
|
23
|
+
const parts = normalized.split('/').filter(Boolean)
|
|
24
|
+
const accountIdx = parts.indexOf('account')
|
|
25
|
+
if (accountIdx === -1) return { section: 'personal' }
|
|
26
|
+
|
|
27
|
+
const sub = parts.slice(accountIdx + 1)
|
|
28
|
+
if (sub.length === 0) return { section: 'personal' }
|
|
29
|
+
if (sub[0] === 'addresses') return { section: 'addresses' }
|
|
30
|
+
if (sub[0] === 'devices') return { section: 'devices' }
|
|
31
|
+
if (sub[0] === 'orders') {
|
|
32
|
+
if (sub[1]) return { section: 'orders', orderId: decodeURIComponent(sub[1]) }
|
|
33
|
+
return { section: 'orders' }
|
|
34
|
+
}
|
|
35
|
+
return { section: 'personal' }
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Área de cuenta cerrada de Ecomiq (`/account`, `/account/addresses`, …).
|
|
3
|
+
* Los templates no pueden overridear estas rutas desde `pages/account`.
|
|
4
|
+
* Login/register viven en la raíz del template (`/login`, `/register`).
|
|
5
|
+
*/
|
|
6
|
+
import { definePage } from '../index'
|
|
7
|
+
import { AccountPageView } from './account/account-page-view'
|
|
8
|
+
|
|
9
|
+
export default definePage({
|
|
10
|
+
meta: () => ({
|
|
11
|
+
title: 'Mi cuenta',
|
|
12
|
+
description: 'Gestiona tu cuenta, direcciones y pedidos',
|
|
13
|
+
noIndex: true,
|
|
14
|
+
}),
|
|
15
|
+
component: AccountPageView,
|
|
16
|
+
})
|
|
@@ -34,7 +34,7 @@ function CheckoutPage({ params }: { params: { checkoutId: string } }) {
|
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
if (!needsAuth || !checkoutId) return
|
|
36
36
|
void navigate({
|
|
37
|
-
to: '/
|
|
37
|
+
to: '/login',
|
|
38
38
|
search: { redirect: `/checkout/${checkoutId}` },
|
|
39
39
|
replace: true,
|
|
40
40
|
})
|
package/src/providers/auth.tsx
CHANGED
|
@@ -62,8 +62,17 @@ export function useAuth() {
|
|
|
62
62
|
session: sessionQuery.data,
|
|
63
63
|
isPending: sessionQuery.isPending,
|
|
64
64
|
error: sessionQuery.error,
|
|
65
|
-
signIn: (credentials: EcomiqSignInCredentials) =>
|
|
66
|
-
signInWithEcomiqCredentials(client, settings, credentials)
|
|
65
|
+
signIn: async (credentials: EcomiqSignInCredentials) => {
|
|
66
|
+
await signInWithEcomiqCredentials(client, settings, credentials)
|
|
67
|
+
const result = await client.getSession()
|
|
68
|
+
const session = (result as { data?: StorefrontSession | null })?.data ?? null
|
|
69
|
+
if (!session?.user) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'No se pudo establecer la sesión. Revisa cookies / certificado HTTPS e inténtalo de nuevo.',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return session
|
|
75
|
+
},
|
|
67
76
|
signOut: () => client.signOut(),
|
|
68
77
|
}
|
|
69
78
|
}
|
package/src/proxy-api.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createApi, type Api } from '@ecomiq/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cliente HTTP hacia el BFF same-origin (`/api/*`).
|
|
5
|
+
* El proxy inyecta `x-Store-Id` y el Bearer desde la cookie OAuth
|
|
6
|
+
* (`ecomiq_sf_oauth_at`, path `/api`).
|
|
7
|
+
*
|
|
8
|
+
* Usar con los services de `@ecomiq/core` (p. ej. `getCustomerAddresses(api)`).
|
|
9
|
+
* `version` vacío: el path del proxy ya es `/api/...` (el upstream añade `/v1`).
|
|
10
|
+
*/
|
|
11
|
+
export function createStorefrontProxyApi(): Api {
|
|
12
|
+
const origin =
|
|
13
|
+
typeof window !== 'undefined' && window.location?.origin
|
|
14
|
+
? window.location.origin
|
|
15
|
+
: 'http://localhost'
|
|
16
|
+
return createApi({
|
|
17
|
+
baseUrl: `${origin}/api`,
|
|
18
|
+
version: '',
|
|
19
|
+
})
|
|
20
|
+
}
|
package/src/vite-inline.mjs
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* Alineado con el ejemplo oficial TanStack Start + Cloudflare:
|
|
8
8
|
* https://github.com/TanStack/router/tree/main/examples/react/start-basic-cloudflare
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, isAbsolute, resolve } from 'node:path'
|
|
11
12
|
import { fileURLToPath } from 'node:url'
|
|
12
13
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
13
14
|
import viteReact from '@vitejs/plugin-react'
|
|
14
15
|
import tailwindcss from '@tailwindcss/vite'
|
|
15
16
|
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
17
|
+
import basicSsl from '@vitejs/plugin-basic-ssl'
|
|
16
18
|
|
|
17
19
|
const postcssBrowserStub = fileURLToPath(
|
|
18
20
|
new URL('./runtime/postcss-browser-stub.mjs', import.meta.url),
|
|
@@ -97,6 +99,51 @@ export function resolveDevRuntime({ config, runtime } = {}) {
|
|
|
97
99
|
return runtime ?? config?.dev?.runtime ?? 'node'
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
/**
|
|
103
|
+
* HTTPS en `ecomiq dev` (localhost).
|
|
104
|
+
* - `dev.https: true` → certificado auto (plugin-basic-ssl)
|
|
105
|
+
* - `dev.https: { cert, key }` → PEM propios (paths relativos al project root)
|
|
106
|
+
*
|
|
107
|
+
* @param {{ config?: object, projectRoot?: string, appDir?: string }} opts
|
|
108
|
+
* @returns {{ enabled: boolean, plugin?: object, https?: import('node:https').ServerOptions }}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveDevHttps({ config, projectRoot, appDir } = {}) {
|
|
111
|
+
const https = config?.dev?.https
|
|
112
|
+
if (!https) return { enabled: false }
|
|
113
|
+
|
|
114
|
+
if (https === true) {
|
|
115
|
+
return {
|
|
116
|
+
enabled: true,
|
|
117
|
+
plugin: basicSsl({
|
|
118
|
+
name: 'ecomiq',
|
|
119
|
+
domains: ['localhost', '127.0.0.1', '::1'],
|
|
120
|
+
}),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof https === 'object' && https !== null) {
|
|
125
|
+
const root = projectRoot ?? (appDir ? resolve(appDir, '..') : process.cwd())
|
|
126
|
+
const certPath = resolvePathFromRoot(root, https.cert)
|
|
127
|
+
const keyPath = resolvePathFromRoot(root, https.key)
|
|
128
|
+
return {
|
|
129
|
+
enabled: true,
|
|
130
|
+
https: {
|
|
131
|
+
cert: readFileSync(certPath),
|
|
132
|
+
key: readFileSync(keyPath),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { enabled: false }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolvePathFromRoot(root, value) {
|
|
141
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
142
|
+
throw new Error('dev.https.cert y dev.https.key deben ser paths a archivos PEM')
|
|
143
|
+
}
|
|
144
|
+
return isAbsolute(value) ? value : resolve(root, value)
|
|
145
|
+
}
|
|
146
|
+
|
|
100
147
|
/**
|
|
101
148
|
* @param {{ appDir: string, config?: object, projectRoot?: string, runtime?: 'workerd' | 'node' }} opts
|
|
102
149
|
*/
|
|
@@ -111,9 +158,14 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
|
|
|
111
158
|
]
|
|
112
159
|
|
|
113
160
|
const resolvedRuntime = resolveDevRuntime({ config, runtime })
|
|
161
|
+
const resolvedHttps = resolveDevHttps({ config, projectRoot: root, appDir })
|
|
114
162
|
|
|
115
163
|
const plugins = [tailwindcss()]
|
|
116
164
|
|
|
165
|
+
if (resolvedHttps.plugin) {
|
|
166
|
+
plugins.push(resolvedHttps.plugin)
|
|
167
|
+
}
|
|
168
|
+
|
|
117
169
|
if (resolvedRuntime === 'workerd') {
|
|
118
170
|
plugins.unshift(bundleSsrDependenciesForWorkerd())
|
|
119
171
|
}
|
|
@@ -172,6 +224,7 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
|
|
|
172
224
|
},
|
|
173
225
|
server: {
|
|
174
226
|
fs: { allow },
|
|
227
|
+
...(resolvedHttps.https ? { https: resolvedHttps.https } : {}),
|
|
175
228
|
},
|
|
176
229
|
}
|
|
177
230
|
}
|