@wopr-network/platform-ui-core 1.1.8 → 1.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -103,7 +103,7 @@ describe("BuyCryptoCreditPanel", () => {
103
103
  });
104
104
 
105
105
  mockCreateCryptoCheckout.mockResolvedValue({
106
- url: "https://payram.io/checkout/abc123",
106
+ url: "https://btcpay.example.com/i/abc123",
107
107
  referenceId: "ref-abc123",
108
108
  });
109
109
  mockIsAllowedRedirectUrl.mockReturnValue(true);
@@ -116,8 +116,8 @@ describe("BuyCryptoCreditPanel", () => {
116
116
  await user.click(screen.getByRole("button", { name: "Pay with crypto" }));
117
117
 
118
118
  expect(mockCreateCryptoCheckout).toHaveBeenCalledWith(50);
119
- expect(mockIsAllowedRedirectUrl).toHaveBeenCalledWith("https://payram.io/checkout/abc123");
120
- expect(hrefSetter).toHaveBeenCalledWith("https://payram.io/checkout/abc123");
119
+ expect(mockIsAllowedRedirectUrl).toHaveBeenCalledWith("https://btcpay.example.com/i/abc123");
120
+ expect(hrefSetter).toHaveBeenCalledWith("https://btcpay.example.com/i/abc123");
121
121
  });
122
122
 
123
123
  it("shows error when redirect URL is not allowed", async () => {
@@ -139,7 +139,7 @@ describe("BuyCryptoCreditPanel", () => {
139
139
  ).toBeInTheDocument();
140
140
  });
141
141
 
142
- it("shows Opening PayRam... while checkout is in progress", async () => {
142
+ it("shows Opening checkout... while checkout is in progress", async () => {
143
143
  mockCreateCryptoCheckout.mockReturnValue(
144
144
  new Promise(() => {
145
145
  /* intentionally pending */
@@ -153,7 +153,7 @@ describe("BuyCryptoCreditPanel", () => {
153
153
  await user.click(screen.getByText("$100"));
154
154
  await user.click(screen.getByRole("button", { name: "Pay with crypto" }));
155
155
 
156
- expect(await screen.findByText("Opening PayRam...")).toBeInTheDocument();
156
+ expect(await screen.findByText("Opening checkout...")).toBeInTheDocument();
157
157
  });
158
158
 
159
159
  it("shows error when crypto checkout API call fails", async () => {
@@ -17,4 +17,19 @@ describe("DividendCalculator", () => {
17
17
 
18
18
  expect(screen.getByText(/the earlier you join/i)).toBeInTheDocument();
19
19
  });
20
+
21
+ it("renders without crash when no API context is available (unauthenticated)", async () => {
22
+ // DividendCalculator is a static component with no API dependencies.
23
+ // This test verifies it renders completely in an environment where
24
+ // fetch is stubbed to reject (simulating unauthenticated pricing page).
25
+ // If a future refactor adds data fetching, this test catches regressions.
26
+ const { DividendCalculator } = await import("@/components/pricing/dividend-calculator");
27
+ const { container } = render(<DividendCalculator />);
28
+
29
+ // Component should render two Card sections
30
+ expect(screen.getByTestId("net-cost")).toBeInTheDocument();
31
+ expect(screen.getByText(/early adopter advantage/i)).toBeInTheDocument();
32
+ // No error text or crash indicators
33
+ expect(container.querySelector(".text-red-500")).toBeNull();
34
+ });
20
35
  });
@@ -1,4 +1,4 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
 
4
4
  // Stub fetch globally before any module imports
@@ -13,9 +13,10 @@ vi.mock("@/lib/api-config", () => ({
13
13
  describe("DividendStats", () => {
14
14
  beforeEach(() => {
15
15
  mockFetch.mockReset();
16
+ vi.resetModules();
16
17
  });
17
18
 
18
- it("renders fallback values when API returns null", async () => {
19
+ it("renders fallback dashes when API returns non-ok response (no error message shown)", async () => {
19
20
  mockFetch.mockResolvedValue({
20
21
  ok: false,
21
22
  status: 500,
@@ -26,9 +27,14 @@ describe("DividendStats", () => {
26
27
  const { DividendStats } = await import("@/components/pricing/dividend-stats");
27
28
  render(<DividendStats />);
28
29
 
29
- expect(screen.getByTestId("pool-amount")).toBeInTheDocument();
30
- expect(screen.getByTestId("active-users")).toBeInTheDocument();
31
- expect(screen.getByTestId("projected-dividend")).toBeInTheDocument();
30
+ // Wait for fetch to complete (loaded=true), then verify fallback dashes
31
+ await waitFor(() => {
32
+ expect(screen.getByTestId("pool-amount")).toHaveTextContent("--");
33
+ });
34
+ expect(screen.getByTestId("active-users")).toHaveTextContent("--");
35
+ expect(screen.getByTestId("projected-dividend")).toHaveTextContent("--");
36
+ // fetchDividendStats returns null (no throw) → .catch() never runs → no red error paragraph
37
+ expect(screen.queryByText(/failed to load/i)).not.toBeInTheDocument();
32
38
  });
33
39
 
34
40
  it("renders live data when API succeeds", async () => {
@@ -45,9 +51,12 @@ describe("DividendStats", () => {
45
51
  const { DividendStats } = await import("@/components/pricing/dividend-stats");
46
52
  render(<DividendStats />);
47
53
 
48
- expect(screen.getByTestId("pool-amount")).toBeInTheDocument();
49
- expect(screen.getByTestId("active-users")).toBeInTheDocument();
50
- expect(screen.getByTestId("projected-dividend")).toBeInTheDocument();
54
+ // useCountUp with prefers-reduced-motion: true sets value immediately
55
+ await waitFor(() => {
56
+ expect(screen.getByTestId("pool-amount")).toHaveTextContent("$2500.00");
57
+ });
58
+ expect(screen.getByTestId("active-users")).toHaveTextContent("8,000");
59
+ expect(screen.getByTestId("projected-dividend")).toHaveTextContent("~$0.31");
51
60
  });
52
61
 
53
62
  it("renders fallback dashes when fetch rejects (network error)", async () => {
@@ -56,9 +65,42 @@ describe("DividendStats", () => {
56
65
  const { DividendStats } = await import("@/components/pricing/dividend-stats");
57
66
  render(<DividendStats />);
58
67
 
59
- // fetchDividendStats catches network errors and returns null component shows "--"
60
- expect(screen.getByTestId("pool-amount")).toBeInTheDocument();
61
- expect(screen.getByTestId("active-users")).toBeInTheDocument();
62
- expect(screen.getByTestId("projected-dividend")).toBeInTheDocument();
68
+ // Wait for fetch to complete, then verify fallback dashes
69
+ await waitFor(() => {
70
+ expect(mockFetch).toHaveBeenCalled();
71
+ });
72
+ // fetchDividendStats catches network errors and returns null → component shows "--"
73
+ await waitFor(() => {
74
+ expect(screen.getByTestId("pool-amount")).toHaveTextContent("--");
75
+ });
76
+ expect(screen.getByTestId("active-users")).toHaveTextContent("--");
77
+ expect(screen.getByTestId("projected-dividend")).toHaveTextContent("--");
78
+ });
79
+
80
+ it("renders error message when fetchDividendStats throws", async () => {
81
+ // To hit the component's .catch() branch, mock the API module directly
82
+ // so fetchDividendStats rejects instead of catching internally
83
+ vi.doMock("@/lib/api", async (importOriginal) => {
84
+ const orig = await importOriginal<typeof import("@/lib/api")>();
85
+ return {
86
+ ...orig,
87
+ fetchDividendStats: vi.fn().mockRejectedValue(new Error("Unexpected failure")),
88
+ };
89
+ });
90
+
91
+ const { DividendStats } = await import("@/components/pricing/dividend-stats");
92
+ render(<DividendStats />);
93
+
94
+ await waitFor(() => {
95
+ expect(screen.getByText("Unexpected failure")).toBeInTheDocument();
96
+ });
97
+ // Error text should be red
98
+ expect(screen.getByText("Unexpected failure")).toHaveClass("text-red-500");
99
+ // Data fields should show "--" (never populated)
100
+ expect(screen.getByTestId("pool-amount")).toHaveTextContent("--");
101
+ expect(screen.getByTestId("active-users")).toHaveTextContent("--");
102
+ expect(screen.getByTestId("projected-dividend")).toHaveTextContent("--");
103
+
104
+ vi.unmock("@/lib/api"); // clean up factory registration so future tests are unaffected
63
105
  });
64
106
  });
@@ -19,18 +19,9 @@ describe("isAllowedRedirectUrl", () => {
19
19
  expect(isAllowedRedirectUrl("https://billing.stripe.com/p/session/test_abc")).toBe(true);
20
20
  });
21
21
 
22
- it("allows Coinbase Commerce URLs", () => {
23
- expect(isAllowedRedirectUrl("https://commerce.coinbase.com/charges/ABC123")).toBe(true);
24
- });
25
-
26
- it("allows PayRam URLs", () => {
27
- expect(isAllowedRedirectUrl("https://payram.io/checkout/abc")).toBe(true);
28
- expect(isAllowedRedirectUrl("https://app.payram.io/pay/abc")).toBe(true);
29
- });
30
-
31
- it("allows same-origin URLs", () => {
32
- // jsdom defaults to http://localhost
22
+ it("allows same-origin URLs (BTCPay checkout is same-origin in local dev)", () => {
33
23
  expect(isAllowedRedirectUrl("http://localhost/billing/success")).toBe(true);
24
+ expect(isAllowedRedirectUrl("http://localhost/i/invoice123")).toBe(true);
34
25
  expect(isAllowedRedirectUrl("/billing/success")).toBe(true);
35
26
  });
36
27
 
@@ -58,4 +49,8 @@ describe("isAllowedRedirectUrl", () => {
58
49
  expect(ALLOWED_REDIRECT_ORIGINS).toBeInstanceOf(Set);
59
50
  expect(ALLOWED_REDIRECT_ORIGINS.has("https://checkout.stripe.com")).toBe(true);
60
51
  });
52
+
53
+ it("does not include defunct payment providers", () => {
54
+ expect(ALLOWED_REDIRECT_ORIGINS.has("https://payram.io")).toBe(false);
55
+ });
61
56
  });
@@ -52,7 +52,7 @@ export function BuyCryptoCreditPanel() {
52
52
  Pay with Crypto
53
53
  </CardTitle>
54
54
  <p className="text-xs text-muted-foreground">
55
- Pay with ETH, USDC, USDT, or other cryptocurrencies via PayRam. Minimum $10.
55
+ Pay with BTC or other cryptocurrencies. Minimum $10.
56
56
  </p>
57
57
  </CardHeader>
58
58
  <CardContent className="space-y-4">
@@ -87,7 +87,7 @@ export function BuyCryptoCreditPanel() {
87
87
  variant="outline"
88
88
  className="w-full sm:w-auto"
89
89
  >
90
- {loading ? "Opening PayRam..." : "Pay with crypto"}
90
+ {loading ? "Opening checkout..." : "Pay with crypto"}
91
91
  </Button>
92
92
  </CardContent>
93
93
  </Card>
@@ -1,12 +1,30 @@
1
- /** Origins that are allowed as redirect targets from backend checkout responses. */
2
- export const ALLOWED_REDIRECT_ORIGINS: ReadonlySet<string> = new Set([
1
+ /** Origins that are always allowed as redirect targets from checkout responses. */
2
+ const STATIC_ALLOWED_ORIGINS: readonly string[] = [
3
3
  "https://checkout.stripe.com",
4
4
  "https://billing.stripe.com",
5
- "https://commerce.coinbase.com",
6
- "https://pay.coinbase.com",
7
- "https://payram.io",
8
- "https://app.payram.io",
9
- ]);
5
+ ];
6
+
7
+ /**
8
+ * Build the full allowed origins set, including any configured BTCPay Server origin.
9
+ * BTCPay is self-hosted so its URL varies per deployment — read from env var.
10
+ */
11
+ function getAllowedOrigins(): ReadonlySet<string> {
12
+ const origins = new Set(STATIC_ALLOWED_ORIGINS);
13
+
14
+ // BTCPay Server (self-hosted, URL varies per deployment)
15
+ const btcpayUrl = process.env.NEXT_PUBLIC_BTCPAY_URL?.trim();
16
+ if (btcpayUrl) {
17
+ try {
18
+ origins.add(new URL(btcpayUrl).origin);
19
+ } catch (_err) {
20
+ // Invalid URL — skip silently; BTCPay checkout will fall back to same-origin
21
+ }
22
+ }
23
+
24
+ return origins;
25
+ }
26
+
27
+ export const ALLOWED_REDIRECT_ORIGINS: ReadonlySet<string> = getAllowedOrigins();
10
28
 
11
29
  /**
12
30
  * Returns true if `url` is safe to navigate to.
package/src/proxy.ts CHANGED
@@ -9,7 +9,11 @@ const apiOrigin = process.env.NEXT_PUBLIC_API_URL
9
9
  ? new URL(process.env.NEXT_PUBLIC_API_URL).origin
10
10
  : "";
11
11
 
12
- const isSecureOrigin = process.env.NODE_ENV === "production";
12
+ /**
13
+ * Only add upgrade-insecure-requests when actually serving over HTTPS.
14
+ * Checking NODE_ENV breaks local dev in Docker (NODE_ENV=production but no TLS).
15
+ * Computed per-request in buildCsp() from the request URL protocol.
16
+ */
13
17
 
14
18
  /**
15
19
  * Nonce-based style-src toggle.
@@ -21,7 +25,8 @@ const isSecureOrigin = process.env.NODE_ENV === "production";
21
25
  const NONCE_STYLES_ENABLED = true;
22
26
 
23
27
  /** Build the CSP header value with a per-request nonce. */
24
- function buildCsp(nonce: string): string {
28
+ function buildCsp(nonce: string, requestUrl?: string): string {
29
+ const isHttps = requestUrl ? requestUrl.startsWith("https://") : false;
25
30
  return [
26
31
  "default-src 'self'",
27
32
  `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://js.stripe.com`,
@@ -36,7 +41,7 @@ function buildCsp(nonce: string): string {
36
41
  "base-uri 'self'",
37
42
  "form-action 'self'",
38
43
  "object-src 'none'",
39
- ...(isSecureOrigin ? ["upgrade-insecure-requests"] : []),
44
+ ...(isHttps ? ["upgrade-insecure-requests"] : []),
40
45
  ].join("; ");
41
46
  }
42
47
 
@@ -160,7 +165,7 @@ export default async function middleware(request: NextRequest) {
160
165
 
161
166
  // Generate a per-request nonce for CSP
162
167
  const nonce = crypto.randomUUID();
163
- const cspHeaderValue = buildCsp(nonce);
168
+ const cspHeaderValue = buildCsp(nonce, request.url);
164
169
 
165
170
  /** Apply CSP and cache-busting headers to any response before returning it. */
166
171
  function withCsp(response: NextResponse): NextResponse {