npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sumit-react

v0.2.0

Published

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

Downloads

277

Readme

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: "[email protected]" },
        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