Skip to content
shopifypaymentstheme-extensionsreactuiops

ArmPay's theme app extension: the Pay Now button after checkout

7 min readBy Tigran

ByTigranFounder, EcomHubPublished Updated

Why the thank-you page is the only seam we get

ArmPay is not yet in Shopify's Payments Partner Program; docs/shopify-outreach.md is the message we sent asking to be let in, and the CLI still returns Beta requirements not met when we try to deploy a real payments extension. Until that unblocks, ArmPay has to live in one of the few places Shopify allows third-party code on the customer's side of the store.

That list: the storefront theme, the order status (thank-you) page, and the embedded admin UI. Checkout itself is closed to non-partners. The admin UI is for merchants, not customers. That leaves the order status page — the page Shopify shows after the order is created, before payment has happened in our manual-method flow. That is where ArmPay puts its Pay Now button.

The component that renders that button is a theme app extension. It lives at extensions/armpay-thankyou/ in the repo. It has the strictest runtime in the project: @shopify/ui-extensions keeps our code off the storefront DOM and limits us to the components Shopify chooses to expose.

What a theme app extension actually is

A theme app extension is a small bundle of code that Shopify lets merchants drop into their theme as a block. It is not a custom-built section that needs theme-file edits; the merchant turns it on in the theme editor and it appears at the position they choose. From the developer side, it is a folder of templated assets that the Shopify CLI uploads alongside the rest of the app.

Two dependencies in package.json are the moving parts here: @shopify/ui-extensions and @shopify/ui-extensions-react. They are what we render against. Together they give us a constrained component library — text, button, layout primitives — and a small set of hooks for reading the order context Shopify makes available on the page.

The constraints are the point. We do not get to bring in arbitrary React libraries, our code does not get to touch the storefront's DOM directly, and the markup that renders on the customer's browser is rendered by Shopify, not by us. We get fewer ways to break the storefront and fewer ways to write client-side bugs that bypass the rest of the app. It also means a lot of the things you would normally reach for — fetching a custom URL on mount, doing client-side analytics, mounting a third-party widget — are not available the way they are in a normal Remix route.

The Pay Now button: what it has to do, what it can't

The job of the extension is small. On the order status page, the customer has just placed an order with the "Pay with ArmPay" manual payment method selected; Shopify has created the order with a Payment Pending status; the customer's card has not been charged. We need to:

  • Confirm the merchant has ArmPay configured (the embedded admin step has run, MerchantConfig exists for this shop).
  • Render a Pay Now button that takes the customer to ArmPay's payment route.
  • Pass enough information to the next page that we can call register.do with the right orderNumber, amount, and currency.

What it cannot do — at least not from inside the extension itself — is call register.do. That endpoint requires the merchant's VPOS userName and password from MerchantConfig, and those fields should never reach the customer's browser. The extension therefore does not talk to ArCa directly; it sends the customer to app/routes/pay.tsx, which runs server-side and does have access to app/lib/arca.server.ts.

That split — extension renders the entry, server route does the bank work — is non-negotiable for the offsite model. Putting the VPOS userName / password into the extension to "save a redirect" would push card-authorising credentials into the customer's browser; the redirect to a server-side route is what prevents that.

The handoff to app/routes/pay.tsx

There is no "Shopify, call my Remix route" primitive in the theme app extension model — the handoff is a link with query parameters and nothing else. The Pay Now button is a link element pointed at our app, with parameters that identify the order: the Shopify order id, the shop domain, and a session token-ish identifier that pay.tsx verifies on the server before doing anything else.

The two things that matter on the receiving side:

  • pay.tsx cannot trust the query string alone. The order id is re-verified against Shopify (the order exists, belongs to this shop, is unpaid, and matches the amount we are about to register with the bank). The extension is on the storefront and the storefront is not authenticated.
  • The redirect target points at the public app.ecomhub.am URL, not at any internal one. That is exactly what SHOPIFY_APP_URL is for, and it is one of the three env vars the README requires alongside SHOPIFY_API_KEY and SHOPIFY_API_SECRET.

For the customer the flow looks like: thank-you page renders → Pay Now button → ArmPay's pay.tsx confirms the order and calls register.do → ArCa returns a formUrl → we redirect the customer to the bank's hosted page. Two hops on top of Shopify's own thank-you.

extensions/armpay-thankyou vs extensions/armpay-block

A small discrepancy worth naming, because it has bitten us once already. The README's project tree says extensions/armpay-block/. The actual file tree on disk says extensions/armpay-thankyou/. The directory is the source of truth; the README is stale.

It is on the list and not yet fixed; merchants never read the README, so the mismatch is invisible to them. It does bite a developer who follows the README to add a sibling extension and names it according to the old pattern. The extension that ships is the one in armpay-thankyou.

Mobile vs desktop, and the URL we redirect to

ArCa's register.do takes a pageView parameter that is either DESKTOP or MOBILE, per docs/VPOS-Research.md. That is one of the few render hints the bank's hosted page accepts. The right thing for the extension is to pass an indication of the customer's device down to pay.tsx so that the eventual register.do call requests the correct page view; the wrong thing is to let the bank guess from a user-agent header we never set explicitly.

Today, the extension's button is the same on desktop and mobile, and pay.tsx makes its own decision about pageView from the inbound request. That works, but the path of least surprise is to have the extension annotate the link with ?pageView=mobile or similar when the rendering context tells it the customer is on a phone. That is a small follow-up.

The other small detail: the redirect URL has to be the production app.ecomhub.am. During development with shopify app dev, the CLI rewrites the public URL to the dev tunnel automatically. The extension does not carry a hard-coded host; the value it links to is the one Shopify injects at install time.

What is still rough

A short, honest list:

  • The README/file-tree discrepancy on the extension name. Cheap fix, not yet done.
  • The Pay Now button is identical in copy across languages. ArCa supports hy | en | ru for the hosted page; the extension does not currently localize its own button. That gap is the only one of these a merchant will hit on day one rather than during App Store review.
  • The empty state — what the extension renders when the merchant has not yet configured ArmPay in the embedded admin — is currently just a hidden block. Arguably it should surface a small note to the merchant, only visible in their own admin preview, telling them what is missing. We have not built that yet.
  • The extension does not pre-fetch the ArCa form URL on render. It generates it only after the click. That keeps the bank-side formUrl lifetime aligned with the customer's actual intent to pay, at the cost of a small spinner gap on first click while pay.tsx makes the register.do call. We are okay with that for now; the alternative is pre-creating ArCa sessions that may never be used, which has its own audit problems.

None of these block the install path or the App Store listing.

What we would do differently with the Payments Partner Program

If we were inside the Payments Partner Program, none of this would look quite the same. A real payments extension lives inside checkout, not after it. The customer would select Pay with ArmPay alongside the platform's native gateways, the redirect to ArCa would happen as part of the checkout flow rather than as a follow-up, and the order would never sit in a Payment Pending state on the order status page — it would either be paid before the order was created, or the order would not be created at all.

The theme app extension exists because we cannot do that yet. It is the workaround that lets the rest of the integration be honest: card data on the bank's page, status reconciled via getOrderStatusExtended.do, payment surfaced to the merchant through orderMarkAsPaid. When the Partner Program invitation lands, the extension does not go away immediately — we still need it for the install flow during the transition — but its job shifts from being the only way ArmPay surfaces to the customer to being a backup for stores that have not migrated to the payments-extension version.

Until that invitation lands, extensions/armpay-thankyou/ is the entire customer-facing surface. The next thing we owe it is localized button copy.