diff --git a/frameworks/remix-run/application/src/routes/$langstore.api.cart.voucher.tsx b/frameworks/remix-run/application/src/routes/$langstore.api.cart.voucher.tsx new file mode 100644 index 0000000..f50d7d9 --- /dev/null +++ b/frameworks/remix-run/application/src/routes/$langstore.api.cart.voucher.tsx @@ -0,0 +1,19 @@ +import { ActionFunction, ActionFunctionArgs } from '@remix-run/node'; +import { getStoreFront } from '~/use-cases/storefront.server'; +import { privateJson } from '~/core/bridge/privateJson.server'; +import { getContext } from '~/use-cases/http/utils'; +import { validateVoucher } from '~/use-cases/crystallize/read/validateVoucher.server'; + +export const action: ActionFunction = async ({ request }: ActionFunctionArgs) => { + const requestContext = getContext(request); + const { secret: storefront } = await getStoreFront(requestContext.host); + const body = await request.json(); + + const checkVoucher = await validateVoucher(body.voucher, { + apiClient: storefront.apiClient, + }); + + return privateJson({ + isValid: checkVoucher, + }); +}; diff --git a/shared/application/translations/en.json b/shared/application/translations/en.json index 24880b0..fcd704b 100644 --- a/shared/application/translations/en.json +++ b/shared/application/translations/en.json @@ -53,7 +53,9 @@ "immutable": "You have initiated, finalized or cancelled a payment, this version of your cart is therefore locked.", "clone": "Clone previous cart", "voucherCode": "Voucher code", - "useVoucher": "Use voucher" + "useVoucher": "Use voucher", + "voucherApplied": "Voucher applied.", + "voucherInvalid": "Voucher is invalid." }, "dimensions": "Dimensions", "specifications": "Manuals and specifications", diff --git a/shared/application/translations/fr.json b/shared/application/translations/fr.json index 661eaa6..b04b552 100644 --- a/shared/application/translations/fr.json +++ b/shared/application/translations/fr.json @@ -53,7 +53,9 @@ "immutable": "Vous avez initié, finalisé ou annulé un paiement, cette version de votre caddie est donc bloquée.", "clone": "Cloner le caddie précédent", "voucherCode": "Bon de réduction", - "useVoucher": "Utiliser bon de réduction" + "useVoucher": "Utiliser bon de réduction", + "voucherApplied": "Bon de réduction appliqué.", + "voucherInvalid": "Le bon de réduction est invalide." }, "dimensions": "Dimensions", "specifications": "Manuels et caractéristiques", diff --git a/shared/application/translations/no.json b/shared/application/translations/no.json index 6312210..96b1116 100644 --- a/shared/application/translations/no.json +++ b/shared/application/translations/no.json @@ -53,7 +53,9 @@ "immutable": "Du har igangsatt, avsluttet eller kansellert en betaling, denne versjonen av handlekurven din er derfor låst.", "clone": "Klone forrige handlekurv", "voucherCode": "Rabatt code", - "useVoucher": "Bruk rabatt" + "useVoucher": "Bruk rabatt", + "voucherApplied": "Rabatt brukt.", + "voucherInvalid": "Rabattkoden er ugyldig." }, "dimensions": "Dimensjoner", "specifications": "Manualer og spesifikasjoner", diff --git a/shared/application/ui/components/voucher.tsx b/shared/application/ui/components/voucher.tsx index ef35c54..ed2dfb7 100644 --- a/shared/application/ui/components/voucher.tsx +++ b/shared/application/ui/components/voucher.tsx @@ -8,11 +8,21 @@ import { useRemoteCart } from '../hooks/useRemoteCart'; import { Input } from './input'; export const VoucherForm: React.FC = () => { - const { cart: localCart, setVoucher } = useLocalCart(); + const { cart: localCart, setVoucher, validateVoucher } = useLocalCart(); const { loading } = useRemoteCart(); const [voucherValue, setVoucherValue] = useState(localCart?.extra?.voucher ?? ''); + const [showMessage, setShowMessage] = useState(''); + const { _t } = useAppContext(); + const checkVoucher = async (voucher: string) => { + if (!voucher) return; + const checkVoucher = await validateVoucher(voucher); + return checkVoucher.isValid + ? setShowMessage(_t('cart.voucherApplied')) + : setShowMessage(_t('cart.voucherInvalid')); + }; + return (
@@ -33,6 +43,7 @@ export const VoucherForm: React.FC = () => { className="bg-[#000] text-[#fff] px-2 py-1 rounded mt-5 text-center h-10" onClick={() => { setVoucher(voucherValue); + checkVoucher(voucherValue); }} > {loading && localCart?.extra?.voucher ? _t('loading') : _t('cart.useVoucher')} @@ -44,12 +55,14 @@ export const VoucherForm: React.FC = () => { onClick={() => { setVoucherValue(''); setVoucher(''); + setShowMessage(''); }} > {_t('delete')} )}
+ {showMessage &&

{showMessage}

}
); diff --git a/shared/application/ui/hooks/useLocalCart.ts b/shared/application/ui/hooks/useLocalCart.ts index 3d85c2f..501ffec 100644 --- a/shared/application/ui/hooks/useLocalCart.ts +++ b/shared/application/ui/hooks/useLocalCart.ts @@ -1,6 +1,8 @@ 'use client'; import { useLocalStorage, writeStorage } from '@rehooks/local-storage'; import { LocalCart } from '~/use-cases/contracts/LocalCart'; +import { ServiceAPI } from '~/use-cases/service-api'; +import { useAppContext } from '../app-context/provider'; const InitializeEmptyLocalCart = (): LocalCart => { return { @@ -18,6 +20,7 @@ export function useLocalCart() { ...cart, }); }; + const { state: appContextState } = useAppContext(); const isImmutable = () => { return cart.state === 'placed'; @@ -106,5 +109,13 @@ export function useLocalCart() { }, }); }, + validateVoucher: async (voucher: string) => { + const api = ServiceAPI({ + language: appContextState.language, + serviceApiUrl: appContextState.serviceApiUrl, + }); + + return await api.validateVoucher(voucher); + }, }; } diff --git a/shared/application/use-cases/contracts/RemoteCart.ts b/shared/application/use-cases/contracts/RemoteCart.ts index 5ce3496..dacc855 100644 --- a/shared/application/use-cases/contracts/RemoteCart.ts +++ b/shared/application/use-cases/contracts/RemoteCart.ts @@ -17,4 +17,14 @@ export type Cart = { }; state: 'cart' | 'placed' | 'ordered'; orderId?: string; + context?: { + price: { + markets?: string[]; + decimals: number; + selectedVariantIdentifier: string; + fallbackVariantIdentifiers: string[]; + compareAtVariantIdentifier: string; + voucherCode?: string; + }; + }; }; diff --git a/shared/application/use-cases/crystallize/read/validateVoucher.server.ts b/shared/application/use-cases/crystallize/read/validateVoucher.server.ts new file mode 100644 index 0000000..9139128 --- /dev/null +++ b/shared/application/use-cases/crystallize/read/validateVoucher.server.ts @@ -0,0 +1,29 @@ +import type { ClientInterface } from '@crystallize/js-api-client'; +import { jsonToGraphQLQuery } from 'json-to-graphql-query'; + +type Deps = { + apiClient: ClientInterface; +}; + +export const validateVoucher = async (voucher: string, { apiClient }: Deps) => { + const query = { + validateVoucher: { + __args: { + voucher, + }, + isValid: true, + }, + }; + + const rawQuery = jsonToGraphQLQuery({ query }); + + try { + const response = await apiClient.shopCartApi(rawQuery); + return response.validateVoucher.isValid; + } catch (exception: any) { + console.error('Failed to fetch cart', exception.message); + return { + valid: false, + }; + } +}; diff --git a/shared/application/use-cases/crystallize/write/editCart.ts b/shared/application/use-cases/crystallize/write/editCart.ts index 7e7bfde..501c5bc 100644 --- a/shared/application/use-cases/crystallize/write/editCart.ts +++ b/shared/application/use-cases/crystallize/write/editCart.ts @@ -8,9 +8,10 @@ type Deps = { type Input = { items: Array<{ sku: string; quantity: number }>; - context: Record; + context: Cart['context']; id?: string; }; + export const hydrateCart = async ( items: Array<{ sku: string; quantity: number }>, { apiClient }: Deps, diff --git a/shared/application/use-cases/service-api/index.ts b/shared/application/use-cases/service-api/index.ts index 38ede3c..efb798e 100644 --- a/shared/application/use-cases/service-api/index.ts +++ b/shared/application/use-cases/service-api/index.ts @@ -108,6 +108,10 @@ export const ServiceAPI = ({ locale, language, serviceApiUrl }: ServiceAPIContex withImages: true, extra: cart.extra, }), + validateVoucher: (voucher: string) => + postJson(serviceApiUrl + '/cart/voucher', { + voucher, + }), // THIS SHOULD BE REMOVED IN A REAL PROJECT sendPaidOrderWithCrystalCoin: (cart: LocalCart, customer: Partial) => sendPaidOrderWithCrystalCoin(serviceApiUrl, language, cart, customer),