OpenCart Mobile App · 12 min read · Updated

OpenCart Mobile App Developer Guide, Part 4: Design Patterns, Hooks and End-to-End Flows

Design patterns and React hooks used in the OpenCart mobile app, with end-to-end flows for login, cart, wishlist, checkout, online payment and orders.

OpenCart Mobile App Developer Guide, Part 4

This is part 4 of 5 of the OpenCart Mobile App Developer Guide. New to the project? Start with Part 1: Overview, Architecture, Repository Layout and Languages.

In this post


10. Design patterns

PatternWhereWhy
Backend for Frontend (BFF)The mobile_api extensionEndpoints are shaped for the app’s screens (catalog.home returns all home sections in one call, productCard is a ready-to-render shape), so the app makes fewer, simpler calls.
Template Method / base classApiControllerShared steps (guard, envelope, decoding, price and image helpers) live in one place. Each endpoint only writes its own logic.
Guard clauseif (!$this->guard()) return; at the top of every methodAuthentication can’t be forgotten in the middle of a method, and failure paths exit early.
Adapter / delegation to corecart.add → api/cart.addProduct, totals → api/cart.getTotals, offline payments → extension/opencart/payment/<code>.confirmReuses OpenCart’s tested logic, so every installed shipping, payment and total extension works without changes.
Middleware / interceptorStartup action (server); request() (client)Cross-cutting concerns (session, currency, CORS, headers, token persistence, error mapping) handled once, outside the business code.
Uniform response envelopeok() / fail() ↔ request()One parser on the client. Field errors map straight onto form fields.
Server-authoritative stateCart, checkout, pricesThe client never computes prices or totals; it displays what the server returns. This prevents price tampering and keeps the app and website consistent.
State machine in the sessionCheckoutEach step stores its result in the session (customer → addresses → shipping_method → payment_method → order_id). Changing an earlier step clears the later ones, so the order is always built from a valid sequence.
Repository / service moduleapi/endpoints.tsOne typed function per endpoint. Screens don’t know URLs.
Provider / ContextStoreProviderApp-wide session data without prop drilling.
Custom hooksuseCart, useAddToCart, useToggleWishlist, useStoreEncapsulate cache keys and update rules, so every screen updates the cart in the same way.
Cache-aside with write-through on mutationuseCartMutation → setQueryDataMutations return fresh server data and put it straight into the cache.
Container / presentational splitScreens fetch; components such as ProductCard, Price and AddressForm mostly receive propsComponents are reusable and easy to reason about.
Lazy module loadingrequire() of the navigator in App.tsxLets runtime theme values apply to module-level styles.
Typed contractstypes.ts, navigation/types.tsCompile-time checking of the API and navigation contract.

11. Hooks: which ones are used and why

11.1 React built-in hooks

HookUsed forExample
useStateLocal UI state: form fields, selected options, quantity, field errors, search text, carousel index, WebView heightconst [code, setCode] = useState('') in CartScreen
useEffectSyncing with something outside render: search debounce (setTimeout), filling the coupon field when the cart loads, loading init on mountSearchScreen waits 400 ms after typing before setting the query term
useLayoutEffectSetting header buttons before the first paint so they don’t flickerPaymentScreen’s “Cancel” header button
useMemoStable derived values: the Context value object, the navigation themeStops every consumer of useStore() re-rendering on each parent render
useCallbackStable function identities in the Context (reload, login, logout, changeCurrency)So the useMemo above stays stable
useRefA mutable flag that must not trigger a renderfinished in PaymentScreen stops completing the order twice when several WebView events fire
useContextReading StoreContext (wrapped by useStore)n/a

11.2 React Query hooks

HookUsed forWhy
useQueryEvery read: home, product, cart, wishlist, orders, addresses, countries, checkout stateCaching, loading and error flags, refetch, enabled for dependent queries (e.g. shipping methods only after there is a shipping address)
useInfiniteQueryProduct lists, reviews, ordersPage-based infinite scroll with getNextPageParam: last.page < last.pages ? last.page + 1 : undefined
useMutationEvery write: add to cart, login, save address, confirm order…isPending for button spinners, onSuccess to update the cache, onError to show errors
useQueryClientManual cache work: setQueryData, invalidateQueries, removeQueries, clearSee the cache rules in §9.5

11.3 React Navigation hooks

HookUsed for
useNavigationNavigating from components that aren’t screens, or from tab screens to stack screens
Screen props (route, navigation)Reading typed parameters and calling navigate, replace, reset and setOptions

11.4 Custom hooks in this project

HookFileWhat it gives you
useStore()context/StoreContext.tsxinit, customer, isLoggedIn, loading, error, reload, login, logout, setCustomer, changeCurrency. Throws if used outside the provider.
useCart()hooks/useCart.tsThe cart query (['cart'])
useCartCount()hooks/useCart.tsNumber of items, for the tab badge
useAddToCart(), useUpdateCartItem(), useRemoveCartItem(), useApplyCoupon()hooks/useCart.tsCart mutations built on the private useCartMutation helper. Each writes the returned cart into the cache and invalidates checkout.
useWishlist()hooks/useWishlist.tsThe wishlist query (['wishlist']), which includes product_ids for quick lookups
useToggleWishlist()hooks/useWishlist.tsisInWishlist(id), toggle(id), toggleAsync(id), isPending. Adds or removes depending on the current state and writes the result into the cache.

Rule of thumb: if two or more screens read or change the same server data, put the query key and the update rule in a hook in src/hooks/. If only one screen uses it, an inline useQuery or useMutation is fine.


12. Flows, end to end

12.1 First launch and session creation

OpenCartSecureStoreAppOpenCartSecureStoreAppEvery later request sends X-Session-Token.The startup reopens that session, so cart and login persist.read oc_session_token (none on first launch)GET common.init (X-Api-Key only)Core starts a new session (new id)data.session_token + header X-Session-Tokensave tokenapplyTheme(init.theme), load navigator

If the session expires on the server (System → Settings → Server → session lifetime), the server silently starts a new one. The response header carries the new id and client.ts stores it. The user then appears as a logged-out guest with an empty cart. This is the same as on the website.

12.2 Browsing

  • Home calls catalog.home once. It returns banners (from the banner chosen in the module settings), top-level categories, featured products (IDs from settings), latest, specials, and bestsellers (only if the core Bestseller module’s table exists).
  • Category → product list calls product.index with category_id, sub_category=1, sort, page and limit. useInfiniteQuery loads the next page when you scroll near the end.
  • Search debounces typing by 400 ms, then calls product.index&search=….
  • Product calls product.info. It returns images, price, special and quantity discounts (with tax applied according to store settings), options (variants inherit their master’s options), subscription plans, attributes, related products, review summary, stock text and the wishlist flag.
  • Prices are empty strings when the store requires login to see prices and nobody is logged in.

12.3 Authentication

Model: login is session-based, exactly like the website. There is no separate JWT. Logging in marks the server-side session (the one identified by X-Session-Token) as belonging to the customer. The token is stored in SecureStore, so the customer stays logged in across app restarts until the session expires or they log out.

DBaccount.loginAppUserDBaccount.loginAppUseralt[wrong password][success]email + passwordPOST {email, password} + X-Session-Tokenguard() (key)logout any current customer2FA enabled? → 403 (2FA is browser-only)login attempts ≥ limit within 1 hour? → 429customer disabled or not approved? → 403$customer->login(email, password)addLoginAttempt(email)401 “No match for E-Mail Address and/or Password”session customer data, clear unpaid checkout statemerge guest wishlist into account, record login IPdeleteLoginAttempts(email){customer}setCustomer, invalidate all queries (prices, cart, wishlist refetch)
  • Cart merge: OpenCart’s cart table is keyed by session id and customer id. After login, core Cart loads the customer’s saved cart together with the guest session’s cart, as on the website.
  • Register (account.register) validates with the same rules and language strings as the website: name lengths, email format and uniqueness, telephone (if required), password length and complexity from store settings, and the account terms checkbox if a terms page is set. If the customer group requires approval, it returns {approval: true, customer: null} and the app shows a message instead of logging in. Otherwise it logs the customer in straight away.
  • Logout (account.logout) calls $customer->logout() and removes the same session keys as the website’s logout. The app then calls queryClient.clear().
  • Forgotten password (account.forgotten) creates a reset token through the core model, which triggers the core email. It returns the same message whether or not the email exists, so it can’t be used to find out which emails have accounts.
  • Change password (account.password) requires the current password.
  • Login-gated screens: endpoints protected by guardCustomer() return 401. The app shows a login prompt on the Account tab. Login and Register accept redirect: 'Checkout' and return the user to checkout afterwards.

12.4 Cart

ActionEndpointServer behaviour
Addcart.add {product_id, quantity, option, subscription_plan_id}Delegates to core api/cart.addProduct: checks the product exists and is enabled, validates required options, option values, variants, minimum quantity and subscription plan. Then resets checkout methods.
Edit quantitycart.edit {key, quantity}Updates the line; quantity 0 or less removes it
Removecart.remove {key}n/a
Couponcart.coupon {coupon}Validated by the core coupon total extension; an empty string removes it
Viewcart.indexLines (options, stock flag, minimum check, subscription text), totals from api/cart.getTotals, weight, warnings, can_checkout

Each response is the full cart. The hooks write it into the cache, so the cart screen and the tab badge update without another request.

Option format: option is an object keyed by product_option_id. Select and radio options take one product_option_value_id. Checkbox options take an array of IDs. Text, textarea, date, time and datetime options take a string.

12.5 Wishlist

  • Guest: product IDs are kept in session['wishlist'].
  • Customer: stored in the database through model_account_wishlist.
  • On login: the guest list is moved into the account.
  • Every endpoint returns {products, product_ids}. useToggleWishlist uses product_ids to decide between add and remove.

12.6 Addresses

address.save validates through validateAddress(), which applies the same rules as the website: postcode required per country, zone required if the country has zones. It creates or updates the address. The first address automatically becomes the default. If the checkout session points at an address that was edited, the session copy is refreshed. Delete refuses the default address, as the website does.

12.7 Checkout

The checkout follows the website’s steps and uses the same session keys, so every installed shipping, payment and order-total extension works unchanged.

logged in, orcheckout.guestcheckout.shippingAddress(if shipping required)checkout.paymentAddress(if required and noshipping)checkout.shippingMethods→checkout.shippingMethodif the store requires apayment addresscheckout.paymentMethods→checkout.paymentMethodcheckout.confirm{comment, agree}offline payment (cod,cheque, bank_transfer,free_checkout)any other gateway →payment_urlweb view reachescheckout/success →checkout.completecheckout/failure or usercancelsNeedCustomerHaveCustomerHaveShippingAddressHavePaymentAddressHaveShippingMethodHavePaymentMethodOrderSavedCompleteWebViewPayment

Step by step:

  1. Customer. A logged-in customer’s details are put in session['customer'] automatically by state(). A guest posts checkout.guest {firstname, lastname, email, telephone}. Guest checkout is only allowed when the store enables it, “login to see prices” is off, and the cart has no downloadable or subscription products (guestAllowed()), the same rule as the website.
  2. Shipping address (only if the cart needs shipping). Either {address_id} from the address book (logged in) or a full address (guest, or a new address with an optional save). Changing it clears the chosen shipping and payment methods, because quotes and taxes depend on the address.
  3. Shipping method. checkout.shippingMethods asks every enabled shipping extension for quotes (model_checkout_shipping_method->getMethods) and stores the quotes in the session. checkout.shippingMethod {code: "flat.flat"} only accepts a code that exists in those stored quotes. The price comes from the stored quote, never from the client.
  4. Payment address (only if System → Settings → Option → Billing address required is on).
  5. Payment method. checkout.paymentMethods lists methods from enabled payment extensions for the payment (or shipping) address and stores them. checkout.paymentMethod {code} accepts only a stored code.
  6. Confirm. checkout.confirm {comment, agree}:
    • requireCheckoutCustomer() and requireCart() (products in the cart, in stock unless the store allows otherwise, minimum quantities met).
    • validateConfirm(): every required step is done, the terms are accepted if a checkout terms page is set, and the shipping data is dropped if the cart no longer needs shipping.
    • saveOrder() builds the order exactly like catalog/controller/checkout/confirm.php: totals from the core total extensions, taxes, customer, addresses, methods, affiliate and marketing tracking, products with subscription data, IP and user agent. It saves with model_checkout_order->addOrder() and order status 0 (missing order). If an unpaid order from an earlier attempt exists in the session, it is updated with editOrder() instead of creating a duplicate.
    • Offline payment (cod, cheque, bank_transfer, free_checkout from the core opencart extension): runs that extension’s own confirm() controller, which sets the order status and sends emails. If it reports success (a redirect to checkout/success), completeOrder() clears the cart and checkout session keys. The response is {status: "complete", order_id}.
    • Any other gateway: the response is {status: "payment_required", payment_url, order_id}.

The app shows all steps on one CheckoutScreen. Each step’s mutation returns the new checkout state, which is written into ['checkout', 'state']. Shipping and payment method lists are separate queries whose keys include the address and method they depend on, so they refetch automatically when those change. They only run once their prerequisites are set (enabled).

12.8 Online payment (WebView)

Payment extension / gatewaycheckout.payWebViewAppPayment extension / gatewaycheckout.payWebViewAppopen payment_url (contains session_token)GET …checkout.pay&session_token=…startup restores session from the query parameterSet-Cookie OCSESSID=<same session>, HttpOnly, SameSite=LaxHTML: order summary + the payment extension’s own form/buttonsuser pays (gateway’s own JS and AJAX use the cookie session)redirect to checkout/success (or checkout/failure)onNavigationStateChange / onShouldStartLoadWithRequest sees “checkout/success”POST checkout.complete (clears cart if order status > 0)reset navigation → OrderSuccess
  • The success and failure URLs are matched with /checkout[/%2F-]+success/i, so both route=checkout/success and SEO URLs work.
  • A useRef flag makes sure the order is only finished once.
  • The Cancel button asks for confirmation. The unpaid order stays at status 0 and is reused on the next confirm.
  • On Expo web, the payment page opens in a new tab and the user taps “I have paid”.

12.9 Orders and reorder

  • order.index returns the customer’s orders, paginated. order.info returns the details. Both use model_account_order, which only returns orders belonging to the logged-in customer.
  • order.reorder {order_id, order_product_id} adds that line (with its options) back to the cart, if the product is still available.

12.10 Reviews

product.review mirrors the website’s validation: reviews enabled, guest reviews allowed or logged in, author 3–25 characters, text 25–1000 characters, rating 1–5. New reviews are saved with status 0, so they wait for admin approval as on the website.

12.11 Currency

The Account tab shows a currency picker when more than one currency is enabled. changeCurrency(code) saves the code to SecureStore, reloads init and invalidates every query. From then on, client.ts adds &currency=XXX to every request, and the startup puts it in the session.

12.12 Theme colours

The admin sets colours → common.init returns theme → App.tsx calls applyTheme() before loading the navigator → every screen’s styles use the store colours. A change takes effect on the next app launch. See §9.2 and §9.7.


← Previous: Part 3: The OpenCart Extension and the Mobile App
Next: Part 5: Configuration, Security, Extending, Testing, Releasing and Troubleshooting →

The series

  1. Part 1: Overview, Architecture, Repository Layout and Languages
  2. Part 2: Tools and Libraries, Why Expo, and the Development Environment
  3. Part 3: The OpenCart Extension and the Mobile App
  4. Part 4: Design Patterns, Hooks and End-to-End Flows (you are reading this part)
  5. Part 5: Configuration, Security, Extending, Testing, Releasing and Troubleshooting
Rupak Nepali
Written byRupak Nepali

Author of four Opencart book.
The recent are Opencart 4 developer book and Opencart 4 user manual

Leave a Reply

Your email address will not be published. Required fields are marked *