Localize order modals, add dual logos, and improve admin order tools.
Farsi business order/variation UI, light/dark logo upload on business profile, payment order steps, variation editing, super-admin site links, and checkout/auth fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
dc570c1e7f
commit
5e799eddf9
@@ -17,7 +17,3 @@
|
||||
.backBtn span {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:global([dir='rtl']) .backBtn svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ interface AuthContextValue {
|
||||
user: AuthUser | null
|
||||
isLoading: boolean
|
||||
login: (cellNumber: string, password: string) => Promise<void>
|
||||
loginWithOtp: (cellNumber: string, code: string) => Promise<void>
|
||||
loginWithOtp: (
|
||||
cellNumber: string,
|
||||
code: string,
|
||||
) => Promise<{ passwordUpdated?: boolean }>
|
||||
logout: () => void
|
||||
setUser: (user: AuthUser) => void
|
||||
}
|
||||
@@ -130,6 +133,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const data = await loginWithOtpRequest(cellNumber, code, getTenantDomain())
|
||||
await assertCustomerAccess(data.user)
|
||||
setUser(data.user)
|
||||
return { passwordUpdated: data.passwordUpdated }
|
||||
}, [])
|
||||
|
||||
const value = useMemo(
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import type { Order } from '../services/orderService'
|
||||
import { getTenantDomain } from '../lib/config'
|
||||
|
||||
export type DeliveryMode = 'delivery' | 'pickup'
|
||||
|
||||
@@ -25,23 +33,94 @@ const defaultState: CheckoutState = {
|
||||
placedOrder: null,
|
||||
}
|
||||
|
||||
function storageKey() {
|
||||
return `meshkee_checkout:${getTenantDomain()}`
|
||||
}
|
||||
|
||||
function readPersisted(): Pick<CheckoutState, 'deliveryMode' | 'selectedAddressId' | 'discountCode'> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(storageKey())
|
||||
if (!raw) return {
|
||||
deliveryMode: defaultState.deliveryMode,
|
||||
selectedAddressId: defaultState.selectedAddressId,
|
||||
discountCode: defaultState.discountCode,
|
||||
}
|
||||
const parsed = JSON.parse(raw) as {
|
||||
deliveryMode?: unknown
|
||||
selectedAddressId?: unknown
|
||||
discountCode?: unknown
|
||||
}
|
||||
return {
|
||||
deliveryMode:
|
||||
parsed.deliveryMode === 'pickup' || parsed.deliveryMode === 'delivery'
|
||||
? parsed.deliveryMode
|
||||
: defaultState.deliveryMode,
|
||||
selectedAddressId:
|
||||
typeof parsed.selectedAddressId === 'string' && parsed.selectedAddressId
|
||||
? parsed.selectedAddressId
|
||||
: null,
|
||||
discountCode:
|
||||
typeof parsed.discountCode === 'string' ? parsed.discountCode : '',
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
deliveryMode: defaultState.deliveryMode,
|
||||
selectedAddressId: defaultState.selectedAddressId,
|
||||
discountCode: defaultState.discountCode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function persistPartial(state: CheckoutState) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
storageKey(),
|
||||
JSON.stringify({
|
||||
deliveryMode: state.deliveryMode,
|
||||
selectedAddressId: state.selectedAddressId,
|
||||
discountCode: state.discountCode,
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
// ignore quota / private mode
|
||||
}
|
||||
}
|
||||
|
||||
const CheckoutContext = createContext<CheckoutContextValue | null>(null)
|
||||
|
||||
export function CheckoutProvider({ children }: { children: ReactNode }) {
|
||||
const [state, setState] = useState<CheckoutState>(defaultState)
|
||||
const [state, setState] = useState<CheckoutState>(() => ({
|
||||
...defaultState,
|
||||
...readPersisted(),
|
||||
}))
|
||||
|
||||
const setDeliveryMode = useCallback((deliveryMode: DeliveryMode) => {
|
||||
setState((prev) => ({ ...prev, deliveryMode }))
|
||||
setState((prev) => {
|
||||
const next = { ...prev, deliveryMode }
|
||||
persistPartial(next)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setSelectedAddressId = useCallback((selectedAddressId: string | null) => {
|
||||
setState((prev) =>
|
||||
prev.selectedAddressId === selectedAddressId ? prev : { ...prev, selectedAddressId },
|
||||
)
|
||||
const normalized =
|
||||
selectedAddressId === null || selectedAddressId === undefined
|
||||
? null
|
||||
: String(selectedAddressId)
|
||||
setState((prev) => {
|
||||
if (prev.selectedAddressId === normalized) return prev
|
||||
const next = { ...prev, selectedAddressId: normalized }
|
||||
persistPartial(next)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setDiscountCode = useCallback((discountCode: string) => {
|
||||
setState((prev) => ({ ...prev, discountCode }))
|
||||
setState((prev) => {
|
||||
const next = { ...prev, discountCode }
|
||||
persistPartial(next)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setPlacedOrder = useCallback((placedOrder: Order | null) => {
|
||||
@@ -50,6 +129,11 @@ export function CheckoutProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const resetCheckout = useCallback(() => {
|
||||
setState(defaultState)
|
||||
try {
|
||||
sessionStorage.removeItem(storageKey())
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [])
|
||||
|
||||
const value = useMemo<CheckoutContextValue>(
|
||||
|
||||
@@ -133,6 +133,8 @@ const en = {
|
||||
'orders.source.operator': 'Operator',
|
||||
'orders.source.app': 'Application',
|
||||
'orders.source.website': 'Website',
|
||||
'orders.step.awaitingPayment': 'Waiting for payment',
|
||||
'orders.step.paymentSuccessful': 'Payment successful',
|
||||
'orders.step.processing': 'Under processing',
|
||||
'orders.step.ready': 'Ready for shipping',
|
||||
'orders.step.shipped': 'Shipped',
|
||||
@@ -344,7 +346,7 @@ const en = {
|
||||
'signup.info.verifyExisting':
|
||||
'Enter the SMS code to join this website. Your Meshkee password and profile stay the same.',
|
||||
'signup.info.verifyExistingPasswordUpdated':
|
||||
'Enter the SMS code to finish joining. Your Meshkee password was updated to the one you just entered.',
|
||||
'Enter the SMS code to finish joining. Your Meshkee password will then be updated to the one you just entered.',
|
||||
'signup.toast.passwordUpdated':
|
||||
'Your Meshkee password was updated to the one you entered on this website.',
|
||||
'signup.cellExists.title': 'Existing Meshkee account',
|
||||
@@ -531,6 +533,8 @@ const fa: Record<MessageKey, string> = {
|
||||
'orders.source.operator': 'اپراتور',
|
||||
'orders.source.app': 'اپلیکیشن',
|
||||
'orders.source.website': 'وبسایت',
|
||||
'orders.step.awaitingPayment': 'در انتظار پرداخت',
|
||||
'orders.step.paymentSuccessful': 'پرداخت موفق',
|
||||
'orders.step.processing': 'در حال پردازش',
|
||||
'orders.step.ready': 'آماده ارسال',
|
||||
'orders.step.shipped': 'ارسالشده',
|
||||
@@ -742,7 +746,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'signup.info.verifyExisting':
|
||||
'کد پیامکی را وارد کنید تا به این وبسایت بپیوندید. رمز عبور و اطلاعات مشکی شما همان میماند.',
|
||||
'signup.info.verifyExistingPasswordUpdated':
|
||||
'کد پیامکی را وارد کنید تا عضویت تمام شود. رمز مشکی شما به رمزی که همین الان وارد کردید بهروز شد.',
|
||||
'کد پیامکی را وارد کنید تا عضویت تمام شود. پس از تأیید، رمز مشکی شما به رمزی که همین الان وارد کردید بهروز میشود.',
|
||||
'signup.toast.passwordUpdated':
|
||||
'رمز مشکی شما به رمزی که در این وبسایت وارد کردید بهروز شد.',
|
||||
'signup.cellExists.title': 'حساب موجود در مشکی',
|
||||
|
||||
@@ -226,27 +226,30 @@ export function LoginPage() {
|
||||
|
||||
setCellExistsSites(null)
|
||||
|
||||
// Password is applied only after OTP when passwordPending; toast then.
|
||||
if (data.passwordUpdated) {
|
||||
showToast(t('signup.toast.passwordUpdated'), 'info')
|
||||
}
|
||||
|
||||
if (data.requiresOtp || data.passwordPending || !data.user.cellVerifiedAt) {
|
||||
if (data.accessToken) {
|
||||
logoutRequest()
|
||||
}
|
||||
await continueToSmsVerify(
|
||||
cellNumber,
|
||||
data.passwordPending
|
||||
? t('signup.info.verifyExistingPasswordUpdated')
|
||||
: t('signup.info.verify'),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.user.customerBusinesses.length === 0) {
|
||||
logoutRequest()
|
||||
setError(CUSTOMER_ACCESS_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.user.cellVerifiedAt) {
|
||||
logoutRequest()
|
||||
await continueToSmsVerify(
|
||||
cellNumber,
|
||||
data.passwordUpdated
|
||||
? t('signup.info.verifyExistingPasswordUpdated')
|
||||
: t('signup.info.verify'),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await login(cellNumber, password)
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
@@ -356,7 +359,10 @@ export function LoginPage() {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await loginWithOtp(cellNumber, smsCode)
|
||||
const result = await loginWithOtp(cellNumber, smsCode)
|
||||
if (result.passwordUpdated) {
|
||||
showToast(t('signup.toast.passwordUpdated'), 'info')
|
||||
}
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
handleApiError(err, t('otp.error.signIn'))
|
||||
|
||||
@@ -117,7 +117,8 @@ export function CheckoutPaymentStep() {
|
||||
setError('')
|
||||
|
||||
if (deliveryMode === 'delivery' && !selectedAddressId) {
|
||||
setError('لطفاً یک آدرس ارسال انتخاب کنید.')
|
||||
showToast('لطفاً یک آدرس ارسال انتخاب کنید.', 'error')
|
||||
navigate('/checkout/delivery', { replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -621,7 +621,7 @@
|
||||
|
||||
.successMeta {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
max-width: 540px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
@@ -645,6 +645,7 @@
|
||||
}
|
||||
|
||||
.successMetaRow dt {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
@@ -653,10 +654,13 @@
|
||||
|
||||
.successMetaRow dd {
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.successMetaValue {
|
||||
|
||||
@@ -39,7 +39,9 @@ export async function register(input: {
|
||||
body: input,
|
||||
})
|
||||
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
if (data.accessToken && data.refreshToken) {
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,18 @@ export interface OrderProcessStep {
|
||||
}
|
||||
|
||||
export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
||||
{
|
||||
id: 'awaiting-payment',
|
||||
label: 'Waiting for payment',
|
||||
labelFa: 'در انتظار پرداخت',
|
||||
color: '#64748B',
|
||||
},
|
||||
{
|
||||
id: 'payment-successful',
|
||||
label: 'Payment successful',
|
||||
labelFa: 'پرداخت موفق',
|
||||
color: '#10B981',
|
||||
},
|
||||
{
|
||||
id: 'processing',
|
||||
label: 'Under processing',
|
||||
|
||||
Reference in New Issue
Block a user