@mmzaini/starling-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mahdi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Starling React
2
+
3
+ Display account details and balances with React 18 or 19. This optional package
4
+ contains no API client, authentication or network requests.
5
+
6
+ ```sh
7
+ npm install @mmzaini/starling-react
8
+ ```
9
+
10
+ ```tsx
11
+ import { AccountCard } from "@mmzaini/starling-react";
12
+ import "@mmzaini/starling-react/styles.css";
13
+
14
+ <AccountCard account={account} balance={balance} identifiers={identifiers} />;
15
+ ```
16
+
17
+ The props accept the data returned by the SDK's `accounts.list()`,
18
+ `accounts.getBalance({ accountUid })` and `accounts.getIdentifiers({ accountUid })`.
19
+ Fetch that data on your server and pass only the authorized display data to the
20
+ browser. The package also works with equivalent JSON from a Python backend.
21
+
22
+ Identifiers are masked by default. Set `masked={false}` to reveal them, or control
23
+ that prop from your own button. Visual masking is not access control: the full
24
+ values are still in your application's memory. `hideBalance` hides the balance too.
25
+ `status="loading"` and `status="error"` provide accessible states.
26
+
27
+ Use `AccountDetails` and `Balance` separately for custom layouts:
28
+
29
+ ```tsx
30
+ import { AccountDetails, Balance } from "@mmzaini/starling-react";
31
+
32
+ <Balance amount={{ currency: "GBP", minorUnits: 12345 }} label="Available" />;
33
+ <AccountDetails identifiers={identifiers} masked />;
34
+ ```
35
+
36
+ `formatMoney({ currency, minorUnits }, locale)` formats integer minor units without
37
+ floating-point division. It supports currency fraction digits, negative values and
38
+ localized output. Unsafe integers are rejected; missing or invalid component amounts
39
+ display an unavailable state. The default locale is `en-GB`, including during SSR.
40
+
41
+ Styles are optional. Import the stylesheet once, or use the `starling-*` classes
42
+ with your own CSS. `AccountCard` accepts `className`, `style`, `title` and footer
43
+ `children`. It displays the effective balance and does not include balances in
44
+ spaces. The components work with SSR and have no dependency on the TypeScript SDK.
45
+
46
+ Run the example from this directory with `npm ci && npm run dev`. It uses dummy
47
+ data and demonstrates masked details, loading and error states.
48
+
49
+ For an example that fetches from Starling on the server, build the TypeScript SDK,
50
+ set `STARLING_ACCESS_TOKEN` to a sandbox token, and run `npm run example:server`.
51
+ Open `http://127.0.0.1:3001`. The server renders masked account details without
52
+ passing credentials to React. It binds only to localhost; add your application's
53
+ session authorization before deploying a similar route.
54
+
55
+ Community maintained; not an official Starling Bank package. [SDK repository](https://github.com/MMZaini/starling-sdk).
package/dist/index.cjs ADDED
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AccountCard: () => AccountCard,
24
+ AccountDetails: () => AccountDetails,
25
+ Balance: () => Balance,
26
+ formatMoney: () => formatMoney
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/money.ts
31
+ function formatMoney(amount, locale = "en-GB") {
32
+ if (!Number.isSafeInteger(amount.minorUnits)) throw new RangeError("minorUnits must be a safe integer");
33
+ if (!/^[A-Z]{3}$/.test(amount.currency)) throw new RangeError("currency must be an uppercase ISO currency code");
34
+ const formatter = new Intl.NumberFormat(locale, { style: "currency", currency: amount.currency });
35
+ const digits = formatter.resolvedOptions().maximumFractionDigits;
36
+ if (digits === void 0) throw new RangeError("Could not determine the currency fraction digits");
37
+ const units = BigInt(amount.minorUnits);
38
+ const absolute = units < 0n ? -units : units;
39
+ const divisor = 10n ** BigInt(digits);
40
+ const whole = absolute / divisor;
41
+ const signedWhole = units < 0n ? whole === 0n ? -0 : -whole : whole;
42
+ const fraction = new Intl.NumberFormat(locale, { useGrouping: false, minimumIntegerDigits: Math.max(1, digits), maximumFractionDigits: 0 }).format(absolute % divisor);
43
+ return formatter.formatToParts(signedWhole).map((part) => part.type === "fraction" ? fraction : part.value).join("");
44
+ }
45
+
46
+ // src/components.tsx
47
+ var import_jsx_runtime = require("react/jsx-runtime");
48
+ function Balance({ amount, label = "Balance", locale = "en-GB", masked = false, className = "" }) {
49
+ let formatted;
50
+ if (amount && !masked) {
51
+ try {
52
+ formatted = formatMoney(amount, locale);
53
+ } catch (error) {
54
+ if (!(error instanceof RangeError)) throw error;
55
+ }
56
+ }
57
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `starling-balance ${className}`, children: [
58
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "starling-label", children: label }),
59
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "starling-balance-value", "aria-label": masked ? `${label} hidden` : formatted ? `${label}: ${formatted}` : `${label} unavailable`, children: masked ? "\u2022\u2022\u2022\u2022" : formatted ?? "\u2014" })
60
+ ] });
61
+ }
62
+ function AccountDetails({ identifiers, masked = true, className = "" }) {
63
+ const rows = [
64
+ ["Account number", identifiers?.accountIdentifier],
65
+ ["Sort code", identifiers?.bankIdentifier],
66
+ ["IBAN", identifiers?.iban],
67
+ ["BIC", identifiers?.bic]
68
+ ].filter((row) => typeof row[1] === "string" && row[1].trim().length > 0);
69
+ if (!rows.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: `starling-empty ${className}`, children: "Account details unavailable" });
70
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", { className: `starling-details ${className}`, children: rows.map(([label, value]) => {
71
+ const compact = value.replace(/\s+/g, "");
72
+ const tail = compact.slice(-Math.min(4, Math.max(0, compact.length - 4)));
73
+ const visibleTail = compact.length > 4 ? tail : "";
74
+ const display = masked ? `\u2022\u2022\u2022\u2022${visibleTail}` : label === "Sort code" && /^\d{6}$/.test(compact) ? compact.match(/.{2}/g).join("-") : value;
75
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "starling-detail", children: [
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: label }),
77
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { "aria-label": masked ? `${label} hidden${visibleTail ? `, ending ${visibleTail}` : ""}` : void 0, children: display })
78
+ ] }, label);
79
+ }) });
80
+ }
81
+ function AccountCard({ account, balance, identifiers, title, status = "ready", masked = true, hideBalance = false, locale = "en-GB", children, className = "", style }) {
82
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: `starling-card ${className}`, style, "aria-label": title ?? account?.name ?? "Account", "aria-busy": status === "loading", children: [
83
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "starling-card-header", children: [
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: title ?? account?.name ?? "Account" }),
85
+ status === "ready" && account?.currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "starling-currency", children: account.currency })
86
+ ] }),
87
+ status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "starling-state", role: "status", children: "Loading account\u2026" }) : status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "starling-state", role: "alert", children: "Unable to load this account." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
88
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Balance, { amount: balance?.effectiveBalance, label: "Effective balance", locale, masked: hideBalance }),
89
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountDetails, { identifiers, masked }),
90
+ children && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("footer", { className: "starling-card-footer", children })
91
+ ] })
92
+ ] });
93
+ }
94
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/money.ts","../src/components.tsx"],"sourcesContent":["export { AccountCard, AccountDetails, Balance } from \"./components.js\";\nexport type { AccountCardProps, AccountDetailsProps, AccountIdentifiers, BalanceProps } from \"./components.js\";\nexport { formatMoney } from \"./money.js\";\nexport type { Money } from \"./money.js\";\n","export interface Money {\n currency: string;\n minorUnits: number;\n}\n\n/** Format integer minor units without floating-point division or cent rounding. */\nexport function formatMoney(amount: Money, locale = \"en-GB\"): string {\n if (!Number.isSafeInteger(amount.minorUnits)) throw new RangeError(\"minorUnits must be a safe integer\");\n if (!/^[A-Z]{3}$/.test(amount.currency)) throw new RangeError(\"currency must be an uppercase ISO currency code\");\n const formatter = new Intl.NumberFormat(locale, { style: \"currency\", currency: amount.currency });\n const digits = formatter.resolvedOptions().maximumFractionDigits;\n if (digits === undefined) throw new RangeError(\"Could not determine the currency fraction digits\");\n const units = BigInt(amount.minorUnits);\n const absolute = units < 0n ? -units : units;\n const divisor = 10n ** BigInt(digits);\n const whole = absolute / divisor;\n const signedWhole = units < 0n ? (whole === 0n ? -0 : -whole) : whole;\n const fraction = new Intl.NumberFormat(locale, { useGrouping: false, minimumIntegerDigits: Math.max(1, digits), maximumFractionDigits: 0 }).format(absolute % divisor);\n return formatter.formatToParts(signedWhole).map((part) => part.type === \"fraction\" ? fraction : part.value).join(\"\");\n}\n","import type { CSSProperties, ReactNode } from \"react\";\nimport { formatMoney, type Money } from \"./money.js\";\n\nexport interface AccountIdentifiers {\n accountIdentifier?: string;\n bankIdentifier?: string;\n iban?: string;\n bic?: string;\n}\n\nexport interface BalanceProps {\n amount?: Money | null;\n label?: string;\n locale?: string;\n masked?: boolean;\n className?: string;\n}\n\nexport function Balance({ amount, label = \"Balance\", locale = \"en-GB\", masked = false, className = \"\" }: BalanceProps) {\n let formatted: string | undefined;\n if (amount && !masked) {\n try { formatted = formatMoney(amount, locale); }\n catch (error) { if (!(error instanceof RangeError)) throw error; }\n }\n return <div className={`starling-balance ${className}`}>\n <p className=\"starling-label\">{label}</p>\n <p className=\"starling-balance-value\" aria-label={masked ? `${label} hidden` : formatted ? `${label}: ${formatted}` : `${label} unavailable`}>\n {masked ? \"••••\" : formatted ?? \"—\"}\n </p>\n </div>;\n}\n\nexport interface AccountDetailsProps {\n identifiers?: AccountIdentifiers | null;\n /** Visual masking only. Full values must still be authorized for this user. */\n masked?: boolean;\n className?: string;\n}\n\nexport function AccountDetails({ identifiers, masked = true, className = \"\" }: AccountDetailsProps) {\n const rows = [\n [\"Account number\", identifiers?.accountIdentifier],\n [\"Sort code\", identifiers?.bankIdentifier],\n [\"IBAN\", identifiers?.iban],\n [\"BIC\", identifiers?.bic],\n ].filter((row): row is [string, string] => typeof row[1] === \"string\" && row[1].trim().length > 0);\n if (!rows.length) return <p className={`starling-empty ${className}`}>Account details unavailable</p>;\n return <dl className={`starling-details ${className}`}>\n {rows.map(([label, value]) => {\n const compact = value.replace(/\\s+/g, \"\");\n const tail = compact.slice(-Math.min(4, Math.max(0, compact.length - 4)));\n const visibleTail = compact.length > 4 ? tail : \"\";\n const display = masked ? `••••${visibleTail}` : label === \"Sort code\" && /^\\d{6}$/.test(compact) ? compact.match(/.{2}/g)!.join(\"-\") : value;\n return <div className=\"starling-detail\" key={label}>\n <dt>{label}</dt>\n <dd aria-label={masked ? `${label} hidden${visibleTail ? `, ending ${visibleTail}` : \"\"}` : undefined}>{display}</dd>\n </div>;\n })}\n </dl>;\n}\n\nexport interface AccountCardProps {\n account?: { name?: string; currency?: string } | null;\n balance?: { effectiveBalance?: Money } | null;\n identifiers?: AccountIdentifiers | null;\n title?: string;\n status?: \"ready\" | \"loading\" | \"error\";\n masked?: boolean;\n hideBalance?: boolean;\n locale?: string;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/** Display-only account summary. Fetch and authorize its data on your server. */\nexport function AccountCard({ account, balance, identifiers, title, status = \"ready\", masked = true, hideBalance = false, locale = \"en-GB\", children, className = \"\", style }: AccountCardProps) {\n return <section className={`starling-card ${className}`} style={style} aria-label={title ?? account?.name ?? \"Account\"} aria-busy={status === \"loading\"}>\n <header className=\"starling-card-header\">\n <h2>{title ?? account?.name ?? \"Account\"}</h2>\n {status === \"ready\" && account?.currency && <span className=\"starling-currency\">{account.currency}</span>}\n </header>\n {status === \"loading\" ? <p className=\"starling-state\" role=\"status\">Loading account…</p>\n : status === \"error\" ? <p className=\"starling-state\" role=\"alert\">Unable to load this account.</p>\n : <>\n <Balance amount={balance?.effectiveBalance} label=\"Effective balance\" locale={locale} masked={hideBalance} />\n <AccountDetails identifiers={identifiers} masked={masked} />\n {children && <footer className=\"starling-card-footer\">{children}</footer>}\n </>}\n </section>;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+
4
+ interface Money {
5
+ currency: string;
6
+ minorUnits: number;
7
+ }
8
+ /** Format integer minor units without floating-point division or cent rounding. */
9
+ declare function formatMoney(amount: Money, locale?: string): string;
10
+
11
+ interface AccountIdentifiers {
12
+ accountIdentifier?: string;
13
+ bankIdentifier?: string;
14
+ iban?: string;
15
+ bic?: string;
16
+ }
17
+ interface BalanceProps {
18
+ amount?: Money | null;
19
+ label?: string;
20
+ locale?: string;
21
+ masked?: boolean;
22
+ className?: string;
23
+ }
24
+ declare function Balance({ amount, label, locale, masked, className }: BalanceProps): react.JSX.Element;
25
+ interface AccountDetailsProps {
26
+ identifiers?: AccountIdentifiers | null;
27
+ /** Visual masking only. Full values must still be authorized for this user. */
28
+ masked?: boolean;
29
+ className?: string;
30
+ }
31
+ declare function AccountDetails({ identifiers, masked, className }: AccountDetailsProps): react.JSX.Element;
32
+ interface AccountCardProps {
33
+ account?: {
34
+ name?: string;
35
+ currency?: string;
36
+ } | null;
37
+ balance?: {
38
+ effectiveBalance?: Money;
39
+ } | null;
40
+ identifiers?: AccountIdentifiers | null;
41
+ title?: string;
42
+ status?: "ready" | "loading" | "error";
43
+ masked?: boolean;
44
+ hideBalance?: boolean;
45
+ locale?: string;
46
+ children?: ReactNode;
47
+ className?: string;
48
+ style?: CSSProperties;
49
+ }
50
+ /** Display-only account summary. Fetch and authorize its data on your server. */
51
+ declare function AccountCard({ account, balance, identifiers, title, status, masked, hideBalance, locale, children, className, style }: AccountCardProps): react.JSX.Element;
52
+
53
+ export { AccountCard, type AccountCardProps, AccountDetails, type AccountDetailsProps, type AccountIdentifiers, Balance, type BalanceProps, type Money, formatMoney };
@@ -0,0 +1,53 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+
4
+ interface Money {
5
+ currency: string;
6
+ minorUnits: number;
7
+ }
8
+ /** Format integer minor units without floating-point division or cent rounding. */
9
+ declare function formatMoney(amount: Money, locale?: string): string;
10
+
11
+ interface AccountIdentifiers {
12
+ accountIdentifier?: string;
13
+ bankIdentifier?: string;
14
+ iban?: string;
15
+ bic?: string;
16
+ }
17
+ interface BalanceProps {
18
+ amount?: Money | null;
19
+ label?: string;
20
+ locale?: string;
21
+ masked?: boolean;
22
+ className?: string;
23
+ }
24
+ declare function Balance({ amount, label, locale, masked, className }: BalanceProps): react.JSX.Element;
25
+ interface AccountDetailsProps {
26
+ identifiers?: AccountIdentifiers | null;
27
+ /** Visual masking only. Full values must still be authorized for this user. */
28
+ masked?: boolean;
29
+ className?: string;
30
+ }
31
+ declare function AccountDetails({ identifiers, masked, className }: AccountDetailsProps): react.JSX.Element;
32
+ interface AccountCardProps {
33
+ account?: {
34
+ name?: string;
35
+ currency?: string;
36
+ } | null;
37
+ balance?: {
38
+ effectiveBalance?: Money;
39
+ } | null;
40
+ identifiers?: AccountIdentifiers | null;
41
+ title?: string;
42
+ status?: "ready" | "loading" | "error";
43
+ masked?: boolean;
44
+ hideBalance?: boolean;
45
+ locale?: string;
46
+ children?: ReactNode;
47
+ className?: string;
48
+ style?: CSSProperties;
49
+ }
50
+ /** Display-only account summary. Fetch and authorize its data on your server. */
51
+ declare function AccountCard({ account, balance, identifiers, title, status, masked, hideBalance, locale, children, className, style }: AccountCardProps): react.JSX.Element;
52
+
53
+ export { AccountCard, type AccountCardProps, AccountDetails, type AccountDetailsProps, type AccountIdentifiers, Balance, type BalanceProps, type Money, formatMoney };
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ // src/money.ts
2
+ function formatMoney(amount, locale = "en-GB") {
3
+ if (!Number.isSafeInteger(amount.minorUnits)) throw new RangeError("minorUnits must be a safe integer");
4
+ if (!/^[A-Z]{3}$/.test(amount.currency)) throw new RangeError("currency must be an uppercase ISO currency code");
5
+ const formatter = new Intl.NumberFormat(locale, { style: "currency", currency: amount.currency });
6
+ const digits = formatter.resolvedOptions().maximumFractionDigits;
7
+ if (digits === void 0) throw new RangeError("Could not determine the currency fraction digits");
8
+ const units = BigInt(amount.minorUnits);
9
+ const absolute = units < 0n ? -units : units;
10
+ const divisor = 10n ** BigInt(digits);
11
+ const whole = absolute / divisor;
12
+ const signedWhole = units < 0n ? whole === 0n ? -0 : -whole : whole;
13
+ const fraction = new Intl.NumberFormat(locale, { useGrouping: false, minimumIntegerDigits: Math.max(1, digits), maximumFractionDigits: 0 }).format(absolute % divisor);
14
+ return formatter.formatToParts(signedWhole).map((part) => part.type === "fraction" ? fraction : part.value).join("");
15
+ }
16
+
17
+ // src/components.tsx
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ function Balance({ amount, label = "Balance", locale = "en-GB", masked = false, className = "" }) {
20
+ let formatted;
21
+ if (amount && !masked) {
22
+ try {
23
+ formatted = formatMoney(amount, locale);
24
+ } catch (error) {
25
+ if (!(error instanceof RangeError)) throw error;
26
+ }
27
+ }
28
+ return /* @__PURE__ */ jsxs("div", { className: `starling-balance ${className}`, children: [
29
+ /* @__PURE__ */ jsx("p", { className: "starling-label", children: label }),
30
+ /* @__PURE__ */ jsx("p", { className: "starling-balance-value", "aria-label": masked ? `${label} hidden` : formatted ? `${label}: ${formatted}` : `${label} unavailable`, children: masked ? "\u2022\u2022\u2022\u2022" : formatted ?? "\u2014" })
31
+ ] });
32
+ }
33
+ function AccountDetails({ identifiers, masked = true, className = "" }) {
34
+ const rows = [
35
+ ["Account number", identifiers?.accountIdentifier],
36
+ ["Sort code", identifiers?.bankIdentifier],
37
+ ["IBAN", identifiers?.iban],
38
+ ["BIC", identifiers?.bic]
39
+ ].filter((row) => typeof row[1] === "string" && row[1].trim().length > 0);
40
+ if (!rows.length) return /* @__PURE__ */ jsx("p", { className: `starling-empty ${className}`, children: "Account details unavailable" });
41
+ return /* @__PURE__ */ jsx("dl", { className: `starling-details ${className}`, children: rows.map(([label, value]) => {
42
+ const compact = value.replace(/\s+/g, "");
43
+ const tail = compact.slice(-Math.min(4, Math.max(0, compact.length - 4)));
44
+ const visibleTail = compact.length > 4 ? tail : "";
45
+ const display = masked ? `\u2022\u2022\u2022\u2022${visibleTail}` : label === "Sort code" && /^\d{6}$/.test(compact) ? compact.match(/.{2}/g).join("-") : value;
46
+ return /* @__PURE__ */ jsxs("div", { className: "starling-detail", children: [
47
+ /* @__PURE__ */ jsx("dt", { children: label }),
48
+ /* @__PURE__ */ jsx("dd", { "aria-label": masked ? `${label} hidden${visibleTail ? `, ending ${visibleTail}` : ""}` : void 0, children: display })
49
+ ] }, label);
50
+ }) });
51
+ }
52
+ function AccountCard({ account, balance, identifiers, title, status = "ready", masked = true, hideBalance = false, locale = "en-GB", children, className = "", style }) {
53
+ return /* @__PURE__ */ jsxs("section", { className: `starling-card ${className}`, style, "aria-label": title ?? account?.name ?? "Account", "aria-busy": status === "loading", children: [
54
+ /* @__PURE__ */ jsxs("header", { className: "starling-card-header", children: [
55
+ /* @__PURE__ */ jsx("h2", { children: title ?? account?.name ?? "Account" }),
56
+ status === "ready" && account?.currency && /* @__PURE__ */ jsx("span", { className: "starling-currency", children: account.currency })
57
+ ] }),
58
+ status === "loading" ? /* @__PURE__ */ jsx("p", { className: "starling-state", role: "status", children: "Loading account\u2026" }) : status === "error" ? /* @__PURE__ */ jsx("p", { className: "starling-state", role: "alert", children: "Unable to load this account." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
59
+ /* @__PURE__ */ jsx(Balance, { amount: balance?.effectiveBalance, label: "Effective balance", locale, masked: hideBalance }),
60
+ /* @__PURE__ */ jsx(AccountDetails, { identifiers, masked }),
61
+ children && /* @__PURE__ */ jsx("footer", { className: "starling-card-footer", children })
62
+ ] })
63
+ ] });
64
+ }
65
+ export {
66
+ AccountCard,
67
+ AccountDetails,
68
+ Balance,
69
+ formatMoney
70
+ };
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/money.ts","../src/components.tsx"],"sourcesContent":["export interface Money {\n currency: string;\n minorUnits: number;\n}\n\n/** Format integer minor units without floating-point division or cent rounding. */\nexport function formatMoney(amount: Money, locale = \"en-GB\"): string {\n if (!Number.isSafeInteger(amount.minorUnits)) throw new RangeError(\"minorUnits must be a safe integer\");\n if (!/^[A-Z]{3}$/.test(amount.currency)) throw new RangeError(\"currency must be an uppercase ISO currency code\");\n const formatter = new Intl.NumberFormat(locale, { style: \"currency\", currency: amount.currency });\n const digits = formatter.resolvedOptions().maximumFractionDigits;\n if (digits === undefined) throw new RangeError(\"Could not determine the currency fraction digits\");\n const units = BigInt(amount.minorUnits);\n const absolute = units < 0n ? -units : units;\n const divisor = 10n ** BigInt(digits);\n const whole = absolute / divisor;\n const signedWhole = units < 0n ? (whole === 0n ? -0 : -whole) : whole;\n const fraction = new Intl.NumberFormat(locale, { useGrouping: false, minimumIntegerDigits: Math.max(1, digits), maximumFractionDigits: 0 }).format(absolute % divisor);\n return formatter.formatToParts(signedWhole).map((part) => part.type === \"fraction\" ? fraction : part.value).join(\"\");\n}\n","import type { CSSProperties, ReactNode } from \"react\";\nimport { formatMoney, type Money } from \"./money.js\";\n\nexport interface AccountIdentifiers {\n accountIdentifier?: string;\n bankIdentifier?: string;\n iban?: string;\n bic?: string;\n}\n\nexport interface BalanceProps {\n amount?: Money | null;\n label?: string;\n locale?: string;\n masked?: boolean;\n className?: string;\n}\n\nexport function Balance({ amount, label = \"Balance\", locale = \"en-GB\", masked = false, className = \"\" }: BalanceProps) {\n let formatted: string | undefined;\n if (amount && !masked) {\n try { formatted = formatMoney(amount, locale); }\n catch (error) { if (!(error instanceof RangeError)) throw error; }\n }\n return <div className={`starling-balance ${className}`}>\n <p className=\"starling-label\">{label}</p>\n <p className=\"starling-balance-value\" aria-label={masked ? `${label} hidden` : formatted ? `${label}: ${formatted}` : `${label} unavailable`}>\n {masked ? \"••••\" : formatted ?? \"—\"}\n </p>\n </div>;\n}\n\nexport interface AccountDetailsProps {\n identifiers?: AccountIdentifiers | null;\n /** Visual masking only. Full values must still be authorized for this user. */\n masked?: boolean;\n className?: string;\n}\n\nexport function AccountDetails({ identifiers, masked = true, className = \"\" }: AccountDetailsProps) {\n const rows = [\n [\"Account number\", identifiers?.accountIdentifier],\n [\"Sort code\", identifiers?.bankIdentifier],\n [\"IBAN\", identifiers?.iban],\n [\"BIC\", identifiers?.bic],\n ].filter((row): row is [string, string] => typeof row[1] === \"string\" && row[1].trim().length > 0);\n if (!rows.length) return <p className={`starling-empty ${className}`}>Account details unavailable</p>;\n return <dl className={`starling-details ${className}`}>\n {rows.map(([label, value]) => {\n const compact = value.replace(/\\s+/g, \"\");\n const tail = compact.slice(-Math.min(4, Math.max(0, compact.length - 4)));\n const visibleTail = compact.length > 4 ? tail : \"\";\n const display = masked ? `••••${visibleTail}` : label === \"Sort code\" && /^\\d{6}$/.test(compact) ? compact.match(/.{2}/g)!.join(\"-\") : value;\n return <div className=\"starling-detail\" key={label}>\n <dt>{label}</dt>\n <dd aria-label={masked ? `${label} hidden${visibleTail ? `, ending ${visibleTail}` : \"\"}` : undefined}>{display}</dd>\n </div>;\n })}\n </dl>;\n}\n\nexport interface AccountCardProps {\n account?: { name?: string; currency?: string } | null;\n balance?: { effectiveBalance?: Money } | null;\n identifiers?: AccountIdentifiers | null;\n title?: string;\n status?: \"ready\" | \"loading\" | \"error\";\n masked?: boolean;\n hideBalance?: boolean;\n locale?: string;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/** Display-only account summary. Fetch and authorize its data on your server. */\nexport function AccountCard({ account, balance, identifiers, title, status = \"ready\", masked = true, hideBalance = false, locale = \"en-GB\", children, className = \"\", style }: AccountCardProps) {\n return <section className={`starling-card ${className}`} style={style} aria-label={title ?? account?.name ?? \"Account\"} aria-busy={status === \"loading\"}>\n <header className=\"starling-card-header\">\n <h2>{title ?? account?.name ?? \"Account\"}</h2>\n {status === \"ready\" && account?.currency && <span className=\"starling-currency\">{account.currency}</span>}\n </header>\n {status === \"loading\" ? <p className=\"starling-state\" role=\"status\">Loading account…</p>\n : status === \"error\" ? <p className=\"starling-state\" role=\"alert\">Unable to load this account.</p>\n : <>\n <Balance amount={balance?.effectiveBalance} label=\"Effective balance\" locale={locale} masked={hideBalance} />\n <AccountDetails identifiers={identifiers} masked={masked} />\n {children && <footer className=\"starling-card-footer\">{children}</footer>}\n </>}\n </section>;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mmzaini/starling-react",
3
+ "version": "0.1.0",
4
+ "description": "React components for displaying Starling account details and balances",
5
+ "license": "MIT",
6
+ "author": "MMZaini",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/MMZaini/starling-sdk.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "homepage": "https://github.com/MMZaini/starling-sdk/tree/main/packages/react",
13
+ "bugs": "https://github.com/MMZaini/starling-sdk/issues",
14
+ "keywords": ["starling", "starling-bank", "react", "components"],
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
22
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
23
+ },
24
+ "./styles.css": "./styles.css"
25
+ },
26
+ "files": ["dist", "styles.css", "README.md", "LICENSE"],
27
+ "sideEffects": ["*.css"],
28
+ "publishConfig": { "access": "public" },
29
+ "peerDependencies": { "react": "^18.3.1 || ^19.0.0" },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "node --import tsx --test tests/*.test.tsx",
34
+ "test:browser": "playwright test",
35
+ "dev": "vite example --host 127.0.0.1",
36
+ "example:server": "node --import tsx example/server.tsx",
37
+ "prepublishOnly": "npm run typecheck && npm run build"
38
+ },
39
+ "devDependencies": {
40
+ "@axe-core/playwright": "4.13.0",
41
+ "@playwright/test": "1.63.0",
42
+ "@types/node": "22.20.2",
43
+ "@types/react": "19.3.0",
44
+ "@types/react-dom": "19.3.0",
45
+ "react": "19.3.0",
46
+ "react-dom": "19.3.0",
47
+ "tsup": "8.5.1",
48
+ "tsx": "4.23.13",
49
+ "typescript": "5.9.3",
50
+ "vite": "8.3.0"
51
+ },
52
+ "overrides": { "esbuild": "^0.28.2" }
53
+ }
package/styles.css ADDED
@@ -0,0 +1,28 @@
1
+ .starling-card {
2
+ --starling-text: #192830;
3
+ --starling-muted: #526570;
4
+ --starling-border: #dce4e7;
5
+ --starling-surface: #fff;
6
+ --starling-accent: #006b65;
7
+ box-sizing: border-box;
8
+ color: var(--starling-text);
9
+ background: var(--starling-surface);
10
+ border: 1px solid var(--starling-border);
11
+ border-radius: 1rem;
12
+ padding: 1.5rem;
13
+ font-family: ui-sans-serif, system-ui, sans-serif;
14
+ min-width: 0;
15
+ }
16
+ .starling-card-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
17
+ .starling-card-header h2 { margin: 0; font-size: 1.1rem; font-weight: 650; overflow-wrap: anywhere; }
18
+ .starling-currency { color: var(--starling-accent, #006b65); background: #eaf5f3; padding: .3rem .55rem; border-radius: .4rem; font-size: .75rem; font-weight: 650; }
19
+ .starling-balance { color: var(--starling-text, #192830); padding: 1.7rem 0; }
20
+ .starling-label { color: var(--starling-muted, #526570); margin: 0 0 .4rem; font-size: .85rem; }
21
+ .starling-balance-value { margin: 0; font-size: clamp(1.6rem, 5vw, 2rem); font-weight: 600; letter-spacing: -.04em; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
22
+ .starling-details { margin: 0; color: var(--starling-text, #192830); }
23
+ .starling-detail { display: flex; justify-content: space-between; gap: 1.2rem; padding: .8rem 0; border-top: 1px solid var(--starling-border, #dce4e7); font-size: .875rem; }
24
+ .starling-detail dt { color: var(--starling-muted, #526570); flex-shrink: 0; }
25
+ .starling-detail dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; min-width: 0; }
26
+ .starling-empty, .starling-state { color: var(--starling-muted, #526570); font-size: .875rem; }
27
+ .starling-state { padding: 2rem 0; }
28
+ .starling-card-footer { border-top: 1px solid var(--starling-border); margin-top: .25rem; padding-top: 1.15rem; font-size: .875rem; }