Skip to content

Digitizers/sumit-react

Repository files navigation

sumit-react

npm types license react next

React components and Next.js route helpers for SUMIT (formerly OfficeGuy) payments. The companion to sumit-api.

SUMIT is the billing platform. The actual card clearing is performed by partner processors that SUMIT routes to — Upay is one such clearer, and SUMIT can integrate with others. From the perspective of this package, you talk to SUMIT; processor-level error codes (e.g. Upay_*) only show up inside SUMIT's response bodies.

Ship a working SUMIT checkout flow in a React or Next.js app with two files: a Client Component and a route handler.

Export What it does
<SumitCheckout /> Client component that loads SUMIT's payments.js, renders the card-input form with the correct field names, and produces a one-time SingleUseToken on submit.
useSumitCheckout() Hook for tracking checkout state (idle | submitting | succeeded | failed).
createSumitChargeRoute() Next.js App Router (or any Web-Standard) POST handler factory that calls the SUMIT recurring-charge endpoint with your server credentials and returns a normalized event.
createSumitWebhookRoute() POST handler factory for SUMIT Triggers (JSON, application/x-www-form-urlencoded, and json=… envelope shapes), with optional shared-secret verification.

Card data never touches your server. The component renders a form whose card inputs are read by SUMIT's payments.js directly; only the resulting SingleUseToken is forwarded to your API route.


Contents

  1. Install
  2. Render the checkout (Client Component)
  3. Charge route (server)
  4. Webhook route (server)
  5. SUMIT environment
  6. API surface
  7. Local development
  8. Acknowledgements
  9. License

Install

pnpm add sumit-react sumit-api

react (and optionally next) are peer dependencies of your app. SUMIT's payments.js is loaded from https://app.sumit.co.il/scripts/payments.js at runtime.


1. Render the checkout (Client Component)

"use client";

import { SumitCheckout, useSumitCheckout } from "sumit-react/client";

export function Checkout() {
  const checkout = useSumitCheckout();

  async function handleToken(singleUseToken: string) {
    const res = await fetch("/api/sumit/charge", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        singleUseToken,
        customer: { externalIdentifier: "org_123", name: "Acme Ltd", emailAddress: "billing@example.com" },
        item: { name: "Pro Plan", description: "Monthly", unitPrice: 19, currency: "USD", durationMonths: 1 },
      }),
    });
    if (!res.ok) {
      checkout.handleError(new Error(await res.text()));
      return;
    }
    checkout.handleSuccess();
  }

  return (
    <SumitCheckout
      ref={checkout.ref}
      companyId={Number(process.env.NEXT_PUBLIC_SUMIT_COMPANY_ID)}
      apiPublicKey={process.env.NEXT_PUBLIC_SUMIT_API_PUBLIC_KEY!}
      environment="production"
      language="he-IL"
      onTokenizationStart={checkout.handleStart}
      onToken={handleToken}
      onError={checkout.handleError}
    >
      <button type="submit" disabled={checkout.status === "submitting"}>
        {checkout.status === "submitting" ? "מעבד..." : "שלם"}
      </button>
      {checkout.status === "failed" ? <p role="alert">{checkout.error?.message}</p> : null}
    </SumitCheckout>
  );
}

The component renders the inputs SUMIT expects (og-ccnum, og-expmonth, og-expyear, og-cvv, optional og-citizenid, hidden og-token). You control the surrounding markup and styling via classNames, style, and children (typically a submit button).


2. Charge route (server)

// app/api/sumit/charge/route.ts
import { createSumitChargeRoute } from "sumit-react/next";

export const POST = createSumitChargeRoute({
  companyId: Number(process.env.SUMIT_COMPANY_ID),
  apiKey: process.env.SUMIT_API_KEY!,
  // mode: "recurring" (default) | "oneOff"
  onResult: async (event) => {
    if (event.ok && event.eventType === "recurring.charged") {
      // persist event.customerId, event.recurringItemId, event.paymentId
    }
    if (event.ok && event.eventType === "payment.succeeded") {
      // one-off charge succeeded — persist event.paymentId, event.documentId
    }
  },
});
mode Endpoint Required item fields
"recurring" (default) POST /billing/recurring/charge/ name, description, unitPrice, currency, durationMonths
"oneOff" POST /billing/payments/charge/ name, description, unitPrice, currency

The same <SumitCheckout /> and SingleUseToken work for both — only the route's mode changes.

What the handler does:

Step Behaviour
Validate Checks the JSON body shape (singleUseToken, customer, item).
Build Calls buildRecurringChargePayload or buildOneOffChargePayload (per mode) from sumit-api.
Send POSTs to /billing/recurring/charge/ or /billing/payments/charge/ (per mode).
Normalize Calls normalizeChargeResponse.
Respond 200 success, 402 declined, 400 bad input, 502 upstream failure — sensitive fields redacted.

3. Webhook route (server)

// app/api/sumit/webhook/route.ts
import { createSumitWebhookRoute, verifySumitSharedSecret } from "sumit-react/next";

export const POST = createSumitWebhookRoute({
  verify: verifySumitSharedSecret(process.env.SUMIT_WEBHOOK_SECRET!),
  onEvent: async (event) => {
    // event is a NormalizedSumitEvent — already redacted, safe to log/persist
    if (event.eventType === "sumit.trigger.unmapped") {
      // Store the safe reconciliation fields and decide whether to promote it.
    }
  },
});

Accepts JSON, application/x-www-form-urlencoded, multipart/form-data, and SUMIT's json=<serialized> envelope. Returns 200 on success, 401 when verification fails, 500 (without leaking the original error) when your handler throws.

By default, verifySumitSharedSecret(secret) accepts only the x-sumit-secret header. If an existing SUMIT trigger can only send a URL query secret, opt in explicitly:

verify: verifySumitSharedSecret(process.env.SUMIT_WEBHOOK_SECRET!, { queryParam: "secret" })

Header verification is preferred because query strings are commonly stored in access logs.


SUMIT environment

Environment URL loaded by <SumitCheckout />
production (default) https://app.sumit.co.il/scripts/payments.js
dev http://dev.sumit.co.il/scripts/payments.js

companyId and apiPublicKey are safe to expose to the browser. The apiKey (without "Public") is server-only and must never reach the client.


Security

Concern How it's handled
Card data exposure SUMIT's payments.js reads card fields directly and returns a SingleUseToken. Card numbers, expiry, and CVV never reach your server or your component state.
Server credential leakage The full apiKey lives only in createSumitChargeRoute; ./client and ./next are separate exports so client bundles cannot transitively pull the server secret.
Webhook spoofing verifySumitSharedSecret checks the x-sumit-secret header by default and hashes both the candidate and the secret to a fixed 32-byte digest before comparing — the comparison is constant-time and length-independent, so response timing leaks neither secret content nor secret length. Query-string secrets are opt-in only because URLs commonly land in logs.
Double-submit / token reuse <SumitCheckout /> uses a synchronous ref guard so two rapid submits cannot both fire CreateToken (single-use tokens are exactly that — single-use).
Logging sensitive data Every event the route helpers return passes through redactSumitPayload from sumit-api.

API surface

// from sumit-react/client
SumitCheckout(props): JSX.Element
  props.companyId, apiPublicKey, environment?, language?
  props.requireCvv?, requireCitizenId?
  props.onToken, onError?, onTokenizationStart?, onTokenizationEnd?
  props.classNames?, style?, labels?
useSumitCheckout(): { ref, status, error, token, submit, reset, handleToken, handleSuccess, handleError, handleStart, clearToken }
loadSumitPayments(env?): Promise<SumitPaymentsSdk>
createSingleUseToken(settings): Promise<string>

// from sumit-react/next
createSumitChargeRoute(config): (request: Request) => Promise<Response>
createSumitWebhookRoute(config): (request: Request) => Promise<Response>
verifySumitSharedSecret(secret, options?): SumitWebhookVerifier

Local development

This package has sumit-api as a peer dependency. While sumit-api is being published to npm, the dev dependency in this repo points at file:../sumit-api, so cloning both repos as siblings is the supported local setup:

~/code/
├── sumit-api/        # https://github.com/Digitizers/sumit-api
└── sumit-react/      # this repo

Then:

pnpm install
pnpm typecheck    # tsc --noEmit
pnpm test         # vitest run
pnpm build        # tsc → dist/

Once sumit-api is published, the dev dependency will switch to a regular semver range and CI will install it from the registry.


Acknowledgements

The browser-side API surface (OfficeGuy.Payments.CreateToken and the og-* form fields) was reverse-engineered from the official SUMIT WooCommerce plugin (GPL-2.0+). No code is copied from that plugin; this implementation is independent and MIT-licensed.


License

MIT

About

React components and Next.js route helpers for SUMIT (OfficeGuy) payments.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors