OpenCart Mobile App · 11 min read · Updated

OpenCart Mobile App Developer Guide, Part 3: The OpenCart Extension and the Mobile App

Inside the OpenCart mobile_api extension and the React Native app: request flow, sessions, the API client, caching, navigation, theming and screens.

OpenCart Mobile App Developer Guide, Part 3

This is part 3 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


8. The backend: the mobile_api OpenCart extension

8.1 How a request reaches an endpoint

The app calls URLs such as:

GET  {baseUrl}index.php?route=extension/mobile_api/api/product.info&product_id=40&language=en-gb&currency=USD
POST {baseUrl}index.php?route=extension/mobile_api/api/cart.add      (JSON body)
Headers: X-Api-Key: <key>   X-Session-Token: <token>   Content-Type: application/json

OpenCart’s router maps extension/mobile_api/api/product.info to the class Opencart\Catalog\Controller\Extension\MobileApi\Api\Product in extension/mobile_api/catalog/controller/api/product.php, and calls its info() method. The part after the dot is the method name.

Core modelsApiController (base)API controllerstartup/mobile_apiOpenCart front controllerAppCore modelsApiController (base)API controllerstartup/mobile_apiOpenCart front controllerAppHTTP request with headersCore startups (settings, session with a NEW id, customer, currency, tax, cart)Startup action (registered on install)Route starts with extension/mobile_api/api/? Module enabled?Send CORS headers (if configured), answer OPTIONS with 204session->start(X-Session-Token) if valid and different?currency=XXX → session currencyRe-run startup/customer, currency, tax, then a new Cart objectCall method, e.g. Cart::add()guard(): enabled? API key matches (hash_equals)? Parse JSON bodyCore logic (api/cart.addProduct, model_checkout_order…)ok($data) or fail($msg, $errors, $status)JSON envelope + X-Session-Token + Cache-Control: no-store

8.2 The base class: system/library/api_controller.php

Every endpoint class extends Opencart\System\Library\Extension\MobileApi\ApiController. It provides:

MethodWhat it does
guard(): boolMust be the first call in every public method. Fails with 503 if the module is disabled and 401 if X-Api-Key doesn’t match, using constant-time hash_equals. Then it merges a JSON body into $this->request->post.
guardCustomer(): boolguard(), plus 401 “Please log in to continue.” when no customer is logged in. Used by account, address and order endpoints.
parseJsonBody()Reads php://input when Content-Type is application/json and runs it through $this->request->clean(), the same HTML escaping OpenCart applies to form posts. Core code that reads $this->request->post then works unchanged.
post($key, $default), query($key, $default)Read POST or GET values.
ok(array $data, string $message = '')Sends {"success":true,"data":{…},"message":"…"}. An empty array is sent as {}, so clients always get an object.
fail(string $message, array $errors = [], int $status = 400)Sets the HTTP status and sends {"success":false,"error":"…","errors":{"field":"message"}}.
output() (private)Adds Content-Type: application/json, Cache-Control: no-store and X-Session-Token (the session actually used) to every response.
decode($s), t($key)OpenCart stores text HTML-escaped (&amp;). Native clients need plain text, so every outgoing string is decoded. t() reads a language key and decodes it.
canSeePrice()False when “Login to see prices” (config_customer_price) is on and nobody is logged in. Every price output respects it.
formatPrice($value, $tax_class_id)Adds tax according to the store’s display setting and formats it in the session currency.
image($path, $w, $h)Absolute URL of a resized image (cached by OpenCart), or the placeholder image. The default size comes from the module settings.
productCard($row)The single compact product shape used by every list: home, category, search, wishlist and related products.
validateAddress(), buildSessionAddress(), addressPayload()Shared address rules (the same as account/address), the address array that checkout keeps in the session, and the address shape sent to the client.

Response envelope

// success
{ "success": true, "data": { "cart": "…" }, "message": "Success: You have added …" }

// failure (HTTP 4xx/5xx)
{ "success": false, "error": "E-Mail Address does not appear to be valid!",
  "errors": { "email": "E-Mail Address does not appear to be valid!", "telephone": "…" } }

Status codes used: 400 bad request, 401 bad key or not logged in, 403 not allowed (disabled feature, 2FA on, account not approved), 404 not found, 409 conflict (empty cart, already logged in), 422 validation errors, 429 too many login attempts, 500 order creation failed, 503 API disabled.

8.3 The startup: catalog/controller/startup/mobile_api.php

Why it exists. When a request arrives, OpenCart’s core startups have already started a session from the OCSESSID cookie. A phone sends no cookie, so that would be a brand-new, empty session on every request: no cart and no login. The startup fixes this by reopening the session the app asks for.

What it does, in order (and only for routes starting with extension/mobile_api/api/):

  1. Returns early if the module is disabled.
  2. Sends CORS headers if a CORS Origin is configured. This is only needed for Expo web. It answers OPTIONS preflight requests with 204.
  3. Takes the token from X-Session-Token. For checkout.pay only, it takes it from the session_token query parameter instead, because a WebView’s first page load can’t send custom headers.
  4. If the token matches ^[a-zA-Z0-9]{26,52}$ and differs from the current id, it calls $this->session->start($token).
  5. If ?currency= is a valid enabled currency, it stores it in the session.
  6. If anything changed, it re-runs startup/customer, startup/currency and startup/tax (the same order as system/config/catalog.php) and replaces the cart object. The logged-in customer, currency and tax rates then match the restored session.

The startup is registered by install() in the admin controller and removed by uninstall(). You can see it under System → Settings → Startups (code mobile_api).

8.4 Endpoint files

FileMethodsAuth
common.phpinit: store info, theme colours, settings, currencies, languages, customer, session tokenkey
catalog.phphome, categories, category, manufacturers, informations, informationkey
product.phpindex (list, search, filter, sort, paginate), info (full detail), reviews, review (write)key
cart.phpindex, add, edit, remove, clear, couponkey
wishlist.phpindex, add, remove (session list for guests, database for customers)key
account.phplogin, logout, register, forgotten, profile, edit, password, newsletterkey; profile, edit, password and newsletter need login
address.phpindex, save (create or update), deletelogin
order.phpindex, info, reorderlogin
localisation.phpcountries, zoneskey
checkout.phpindex, guest, shippingAddress, paymentAddress, shippingMethods, shippingMethod, paymentMethods, paymentMethod, confirm, pay (HTML), completekey; checkout steps need a customer (logged in or guest) in the session

The full parameter list is in the README’s API reference.

8.5 The admin module: admin/controller/module/mobile_api.php

  • index() renders the settings form (mobile_api.twig). It generates a 40-character API key if none exists and shows the API base URL for testing.
  • save() checks permission, validates the key (^[a-zA-Z0-9]{20,64}$) and each colour (^#[0-9a-fA-F]{6}$ or empty), normalises the featured product IDs, and saves everything under the setting code module_mobile_api. editSetting replaces the whole group, so every field must be posted together. The form already does this.
  • install() registers the startup and writes default settings (status off and a random key).
  • uninstall() removes the startup and all module_mobile_api_* settings.

Errors are returned as {"error": {"color_primary": "…"}}. OpenCart’s admin JavaScript highlights #input-color-primary and fills #error-color-primary (underscores become dashes).


9. The mobile app

9.1 Layers

Screens (src/screens)          what the user sees; compose components; call hooks
   │
Components (src/components)    reusable UI: Button, TextField, ProductCard, AddressForm, …
   │
Hooks + Context                React Query hooks (useCart, useWishlist, inline useQuery/useMutation)
   │                           StoreContext (init data, customer, login/logout, currency)
Endpoints (src/api/endpoints)  one typed function per server endpoint
   │
Client (src/api/client)        fetch, headers, token storage, envelope unwrap, ApiError

Rules that keep this clean:

  • Screens never call fetch. They call api.* through React Query.
  • Only client.ts knows about headers, tokens and the envelope.
  • Types live in api/types.ts and mirror the PHP output exactly. Change both together.

9.2 Start-up sequence (App.tsx)

App
└─ SafeAreaProvider
   └─ QueryClientProvider        (staleTime 60 s, retry 1)
      └─ StoreProvider           calls api.common.init() once on mount
         └─ Bootstrap
             loading  → BootScreen (spinner)
             error    → BootScreen (message + Try again → reload())
             ready    → applyTheme(init.theme)
                        require('./src/navigation/RootNavigator')   ← loaded only now
                        <NavigationContainer theme={navTheme}>
                          <StatusBar style=light|dark />
                          <RootNavigator/>

Why the navigator is require()d late: each screen builds its styles with StyleSheet.create() when its module first loads, and that copies the colour values at that moment. The store’s colours must be in colors before any screen module loads. App.tsx therefore deliberately imports nothing that creates styles; even the boot screen uses inline styles. Don’t add imports of screens or components to App.tsx. The side effect is that a colour change in the admin shows up on the next app launch, not live.

9.3 API client (src/api/client.ts)

  • request<T>(route, {method, query, body}) is the only way the app talks to the server.
    • Adds Accept, X-Api-Key, X-Session-Token (if one is stored) and Content-Type: application/json for POST.
    • Builds the URL with buildUrl(): route=extension/mobile_api/api/<route>, language, the stored currency, and any query parameters (empty values are dropped).
    • Sends credentials: 'omit', so no cookies are used; the header is the only session carrier.
    • Reads X-Session-Token from every response and stores it if it changed. This is how a new install gets its first session, and how it recovers when the server’s session expired and a new one was created.
    • Unwraps the envelope. On success:false it throws ApiError(message, status, errors). A network failure becomes ApiError(…, 0). A non-JSON response (a PHP error, or the extension not installed) gets a helpful message.
  • Storage: expo-secure-store on device, localStorage on web. Keys are oc_session_token and oc_currency. The values are loaded once (ensureLoaded) and then kept in memory.

9.4 Endpoints and types

src/api/endpoints.ts exports one object:

api.cart.add({ product_id: 40, quantity: 1, option: { 218: '5' } })  // Promise<Cart>
api.checkout.confirm({ comment: '', agree: true })                  // Promise<ConfirmResult>

Grouping by resource mirrors the PHP files, so api.cart.* maps to cart.php. The return types come from src/api/types.ts.

9.5 State management

Kind of stateWhere it livesExamples
Server state (the large majority)React Query cachecatalog, product, cart, wishlist, orders, addresses, checkout state
App-wide session stateStoreContextinit (store, settings, currencies, theme), customer, isLoggedIn, login(), logout(), changeCurrency(), reload()
Screen-local UI stateuseStateform fields, selected options, quantity, search text, image carousel index
PersistedSecureStoresession token, currency

Query keys in use (keep new ones consistent):

KeyData
['home']Home sections
['categories', parentId], ['category', id]Category tree and details
['products', query]Product lists (infinite)
['product', id]Product details
['reviews', productId]Reviews (infinite)
['cart'] (cartKey)Cart
['wishlist'] (wishlistKey)Wishlist
['profile'], ['addresses'], ['orders'], ['order', id]Account
['countries'], ['zones', countryId]Localisation (staleTime: Infinity)
['informations'], ['information', id]Information pages
['checkout', 'state'], ['checkout', 'shipping', addr], ['checkout', 'payment', method, addr]Checkout

Cache rules used across the app:

  • Mutations that return the full cart (add, edit, remove, coupon) write the response straight into ['cart'] with setQueryData. There’s no extra refetch, and the tab badge updates instantly. They also invalidate ['checkout'], because the totals changed.
  • After login, queryClient.invalidateQueries() refetches everything, because prices, cart (merged with the account cart) and wishlist can all change.
  • After logout, queryClient.clear() drops all cached data, so nothing from the previous customer stays visible.
  • After currency change, init reloads and everything is invalidated, so prices refetch in the new currency.
  • After an order is placed, ['cart'] is invalidated and every ['checkout', …] query is removed.

9.6 Navigation

src/navigation/RootNavigator.tsx:

Stack (native-stack)
├─ Tabs (bottom-tabs, header hidden on the stack)
│   ├─ Home         (own header inside the screen)
│   ├─ Categories
│   ├─ Cart         (badge: cart count)
│   ├─ Wishlist     (badge: wishlist count)
│   └─ Account
├─ Product, ProductList, Search, Reviews
├─ WriteReview, Login, Register              (presentation: 'modal')
├─ ForgotPassword
├─ Checkout, Payment (no swipe-back), OrderSuccess (no back)
├─ Orders, OrderDetail, Addresses, AddressEdit, EditProfile, ChangePassword
└─ Information

Every route’s parameters are typed in src/navigation/types.ts (RootStackParamList, TabParamList). Screens get typed props with NativeStackScreenProps<RootStackParamList, 'Product'>. Components use useNavigation<NativeStackNavigationProp<RootStackParamList>>(). A typo in a route name or a missing parameter fails tsc.

Header and tab bar colours come from colors.tertiary and colors.onTertiary. The active tab uses colors.primary (white on a dark tertiary).

9.7 Theme (src/theme.ts)

  • colors, spacing, radius and font are the only places style constants should come from. Don’t hard-code hex values in screens.
  • applyTheme(init.theme) overrides colours from the admin and derives the rest:
Admin settingcolors fields setDerived
PrimaryprimaryprimaryDark (25 % black), primarySoft (90 % white), onPrimary (contrast text)
SecondarysecondaryonSecondary
TertiarytertiaryonTertiary, tertiaryIsDark (controls the status bar style)
FonttexttextMuted (35 % lighter); also onTertiary on light headers
Buttonbutton (falls back to primary)n/a
Button textonButton (falls back to automatic contrast)n/a

The contrast choice (readableOn) uses the WCAG relative-luminance formula and picks whichever of white or near-black has the higher contrast ratio. accent (red) is reserved for errors and destructive actions and can’t be changed from the admin, so errors always look like errors.

9.8 Components (src/components)

ComponentUse
ui.tsxButton (variants: primary, secondary, outline, ghost, danger; loading, icon, small), TextField (label, error), Card, Row, SectionHeader, Chip, Checkbox, RadioRow, ListItem, Badge
States.tsxLoading, ErrorState (with retry), EmptyState (with action)
Price.tsxPrice (normal and special price), Rating (stars)
ProductCard.tsxGrid card: image, sale badge, wishlist heart, price
ProductGrid.tsxTwo-column FlatList with infinite-scroll footer
ProductResults.tsxuseInfiniteQuery around api.product.list plus a sort bar; used by ProductList and Search
ProductOptions.tsxRenders every OpenCart option type (select, radio, checkbox, text, textarea, date, time, datetime). File options show a notice instead.
QuantityStepper.tsx− / + control with a minimum
SelectModal.tsxSearchable picker in a modal (countries, zones, option values)
AddressForm.tsxAddress fields with country and zone pickers (loads zones when the country changes)
HtmlView.tsxRenders store HTML in an auto-sized WebView; links open in the system browser

9.9 Screens (src/screens)

ScreenDataNotes
Home['home']Logo or name, search box, banner carousel, category row, featured, specials, latest, bestsellers
Categories['categories', 0]Expandable tree → ProductList
ProductListProductResultsCategory, manufacturer or specials, with sort
SearchProductResults400 ms debounce before querying
Product['product', id]Gallery, price and discounts, options, subscriptions, quantity, add to cart, wishlist, attributes, description, related products, reviews link
Reviews, WriteReview['reviews', id]Infinite list; the write form validates on the server
CartuseCartEdit quantity, remove, coupon, totals, stock warnings, checkout button
WishlistuseWishlistGrid of saved products
AccountStoreContext, ['profile']Login prompt or profile, newsletter switch, links, currency picker, information pages, logout
Login, Register, ForgotPasswordn/aModal screens; can redirect back to Checkout
Checkout['checkout', …]Step-by-step on one screen (see §12.7)
Paymentn/aWebView for online gateways
OrderSuccessn/aConfirmation; the back stack is reset
Orders, OrderDetail['orders'], ['order', id]History, status timeline, reorder an item
Addresses, AddressEdit['addresses']Address book with set-default
EditProfile, ChangePasswordn/aAccount forms
Information['information', id]CMS pages (About, Terms…)

← Previous: Part 2: Tools and Libraries, Why Expo, and the Development Environment
Next: Part 4: Design Patterns, Hooks and End-to-End Flows →

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 (you are reading this part)
  4. Part 4: Design Patterns, Hooks and End-to-End Flows
  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 *