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:
Alireza Hassani
2026-08-26 00:21:48 +03:30
co-authored by Cursor
parent dc570c1e7f
commit 5e799eddf9
30 changed files with 779 additions and 169 deletions
@@ -17,7 +17,3 @@
.backBtn span {
line-height: 1.4;
}
:global([dir='rtl']) .backBtn svg {
transform: scaleX(-1);
}
+5 -1
View File
@@ -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(
+91 -7
View File
@@ -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>(
+6 -2
View File
@@ -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': 'حساب موجود در مشکی',
+18 -12
View File
@@ -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 {
+3 -1
View File
@@ -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
}
+12
View File
@@ -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',