@meterbox/customer-portal 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -35,6 +35,13 @@ function handlePlanSelect(plan, billingInterval) {
35
35
  />
36
36
  ```
37
37
 
38
+ ## Branding
39
+
40
+ The portal takes your branding from Settings → Branding
41
+ (`GET /v1/customer-portal/:id/branding`): your logo and company name in its
42
+ header, and your colors, font and buttons on every page. Pass `branding` to
43
+ override it.
44
+
38
45
  ## Pages
39
46
 
40
47
  - **Account**: name, email, current plan, status (`GET /v1/customer-portal/:id/me`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meterbox/customer-portal",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Drop-in multi-page React customer portal for Meterbox: Account, Plans, Credits. Built on @meterbox/react.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://meterbox.ai",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@meterbox/react": "0.1.3"
20
+ "@meterbox/react": "0.1.5"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": ">=17",
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const React = require("react");
9
+ const { theme } = require("@meterbox/react");
9
10
 
10
11
  const REASONS = {
11
12
  too_expensive: "It's too expensive",
@@ -17,12 +18,12 @@ const REASONS = {
17
18
 
18
19
  const styles = {
19
20
  box: {
20
- fontFamily: "system-ui, -apple-system, sans-serif",
21
- color: "#111",
21
+ fontFamily: theme.font,
22
+ color: theme.text,
22
23
  border: "1px solid #e5e7eb",
23
24
  borderRadius: 12,
24
25
  padding: 16,
25
- background: "#fff",
26
+ background: theme.card,
26
27
  display: "grid",
27
28
  gap: 10,
28
29
  maxWidth: 480,
@@ -31,13 +32,14 @@ const styles = {
31
32
  row: { display: "flex", gap: 8, flexWrap: "wrap" },
32
33
  button: {
33
34
  padding: "6px 12px",
34
- borderRadius: 8,
35
+ borderRadius: theme.radius(8),
35
36
  border: "1px solid #d1d5db",
36
- background: "#fff",
37
+ background: theme.card,
38
+ color: theme.text,
37
39
  cursor: "pointer",
38
40
  fontSize: 13,
39
41
  },
40
- primary: { background: "#111", color: "#fff", borderColor: "#111" },
42
+ primary: theme.button,
41
43
  danger: { color: "#b91c1c", borderColor: "#fecaca" },
42
44
  offer: { border: "1px solid #e5e7eb", borderRadius: 8, padding: 10, display: "grid", gap: 6 },
43
45
  error: { color: "#dc2626" },
@@ -12,14 +12,20 @@
12
12
  * threshold={10} // (optional) low-balance trigger
13
13
  * currency="USD" // (optional)
14
14
  * granularity="day" // (optional)
15
+ * branding={{ accent_color }} // (optional) instead of the tenant's
15
16
  * />
16
17
  *
18
+ * Branded with the tenant's branding (Settings → Branding, fetched from
19
+ * GET /v1/customer-portal/:id/branding): logo and company name in the
20
+ * header, and every page and component themed through <BrandingProvider>.
21
+ *
17
22
  * No router dependency — page state is internal. Hosts that want
18
23
  * browser-history nav can drive `initialPage` from their own router and
19
24
  * observe `onPageChange`.
20
25
  */
21
26
 
22
27
  const React = require("react");
28
+ const { BrandingProvider, logoUrl, theme } = require("@meterbox/react");
23
29
  const AccountPage = require("./pages/AccountPage");
24
30
  const PlansPage = require("./pages/PlansPage");
25
31
  const CreditsPage = require("./pages/CreditsPage");
@@ -29,8 +35,8 @@ const LABELS = { account: "Account", plans: "Plans", credits: "Credits" };
29
35
 
30
36
  const styles = {
31
37
  shell: {
32
- fontFamily: "system-ui, -apple-system, sans-serif",
33
- color: "#111",
38
+ fontFamily: theme.font,
39
+ color: theme.text,
34
40
  display: "grid",
35
41
  gap: 16,
36
42
  maxWidth: 760,
@@ -52,11 +58,26 @@ const styles = {
52
58
  color: "#374151",
53
59
  },
54
60
  tabActive: {
55
- color: "#5045e8",
56
- borderBottom: "2px solid #5045e8",
61
+ color: theme.accent,
62
+ borderBottom: `2px solid ${theme.accent}`,
57
63
  },
64
+ header: { display: "flex", alignItems: "center", gap: 10 },
65
+ logo: { maxHeight: 32, maxWidth: 160 },
66
+ company: { fontSize: 16, fontWeight: 700 },
58
67
  };
59
68
 
69
+ // The tenant's branding and company name; none (unbranded) if it can't load.
70
+ async function fetchBranding({ baseUrl, customerId, customerToken, fetchImpl }) {
71
+ const url = `${String(baseUrl).replace(/\/+$/, "")}/v1/customer-portal/${encodeURIComponent(
72
+ customerId,
73
+ )}/branding`;
74
+ const res = await fetchImpl(url, {
75
+ method: "GET",
76
+ headers: { Authorization: `Bearer ${customerToken}` },
77
+ });
78
+ return res.ok ? res.json() : null;
79
+ }
80
+
60
81
  function CustomerPortal({
61
82
  baseUrl,
62
83
  customerId,
@@ -68,10 +89,27 @@ function CustomerPortal({
68
89
  threshold,
69
90
  currency,
70
91
  granularity,
92
+ branding,
71
93
  fetch: fetchImpl,
72
94
  }) {
73
95
  const initial = PAGES.includes(initialPage) ? initialPage : "account";
74
96
  const [page, setPage] = React.useState(initial);
97
+ const [tenant, setTenant] = React.useState(null);
98
+
99
+ React.useEffect(() => {
100
+ let alive = true;
101
+ fetchBranding({ baseUrl, customerId, customerToken, fetchImpl: fetchImpl || globalThis.fetch })
102
+ .then((body) => {
103
+ if (alive) setTenant(body);
104
+ })
105
+ .catch(() => {});
106
+ return () => {
107
+ alive = false;
108
+ };
109
+ }, [baseUrl, customerId, customerToken, fetchImpl]);
110
+ const brand = branding || tenant?.branding || null;
111
+ const logo = logoUrl(brand);
112
+ const companyName = tenant?.company_name || null;
75
113
 
76
114
  // Honor external nav changes via initialPage prop.
77
115
  React.useEffect(() => {
@@ -100,27 +138,39 @@ function CustomerPortal({
100
138
  }
101
139
 
102
140
  return React.createElement(
103
- "div",
104
- { className: "meterbox-customer-portal", style: styles.shell },
141
+ BrandingProvider,
142
+ { branding: brand },
105
143
  React.createElement(
106
- "nav",
107
- { style: styles.nav, role: "tablist" },
108
- PAGES.map((p) =>
144
+ "div",
145
+ { className: "meterbox-customer-portal", style: styles.shell },
146
+ (logo || companyName) &&
109
147
  React.createElement(
110
- "button",
111
- {
112
- key: p,
113
- type: "button",
114
- role: "tab",
115
- "aria-selected": page === p,
116
- style: page === p ? { ...styles.tab, ...styles.tabActive } : styles.tab,
117
- onClick: () => go(p),
118
- },
119
- LABELS[p],
148
+ "header",
149
+ { style: styles.header },
150
+ logo &&
151
+ React.createElement("img", { src: logo, alt: companyName || "", style: styles.logo }),
152
+ companyName && React.createElement("span", { style: styles.company }, companyName),
153
+ ),
154
+ React.createElement(
155
+ "nav",
156
+ { style: styles.nav, role: "tablist" },
157
+ PAGES.map((p) =>
158
+ React.createElement(
159
+ "button",
160
+ {
161
+ key: p,
162
+ type: "button",
163
+ role: "tab",
164
+ "aria-selected": page === p,
165
+ style: page === p ? { ...styles.tab, ...styles.tabActive } : styles.tab,
166
+ onClick: () => go(p),
167
+ },
168
+ LABELS[p],
169
+ ),
120
170
  ),
121
171
  ),
172
+ body,
122
173
  ),
123
- body,
124
174
  );
125
175
  }
126
176
 
@@ -5,17 +5,17 @@
5
5
  */
6
6
 
7
7
  const React = require("react");
8
- const { InvoiceHistory } = require("@meterbox/react");
8
+ const { InvoiceHistory, theme } = require("@meterbox/react");
9
9
  const CancelSubscription = require("../CancelSubscription");
10
10
 
11
11
  const styles = {
12
12
  card: {
13
- fontFamily: "system-ui, -apple-system, sans-serif",
14
- color: "#111",
13
+ fontFamily: theme.font,
14
+ color: theme.text,
15
15
  border: "1px solid #e5e7eb",
16
16
  borderRadius: 12,
17
17
  padding: 20,
18
- background: "#fff",
18
+ background: theme.card,
19
19
  display: "grid",
20
20
  gap: 12,
21
21
  maxWidth: 480,