@solvapay/react 2.1.1 → 2.2.0-preview-11dbbe603cd1b9a44cc41b5eee3686bc4000f97f

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/CHANGELOG.md CHANGED
@@ -1,11 +1,32 @@
1
1
  # @solvapay/react changelog
2
2
 
3
- ## 2.1.1
3
+ ## 2.2.0-preview-11dbbe603cd1b9a44cc41b5eee3686bc4000f97f
4
+
5
+ ### Minor Changes
6
+
7
+ - 4771b85: Fix outbound links being silently dropped inside MCP app iframes. Hosts whose iframe sandbox omits `allow-popups` — Claude on web and desktop — discard both `<a target="_blank">` and `window.open()`, so "Manage account", hosted checkout, "Reopen checkout", Terms/Privacy, the mandate links and the seller support link all rendered as live controls that did nothing when clicked. Every outbound link the SDK renders now routes through the host's `ui/open-link` request when the host declares the `openLinks` capability, and keeps its native `href` navigation everywhere else (plain web checkout, MCP Inspector, ChatGPT).
8
+
9
+ Adds `useExternalLinkClick()`, `useOpenExternal()` and `<ExternalLinkProvider>`. `<McpBridgeProvider>` (and therefore `<McpApp>`) mounts the opener automatically from `app.openLink`, so integrators on the turnkey shell get the fix without code changes. Mount `<ExternalLinkProvider>` yourself only when hand-rolling a shell for some other sandboxed host.
10
+
11
+ - ce04a3b: Fix plan switching and clarify the plan card on the MCP account surface.
12
+
13
+ The account view only rendered `<CurrentPlanCard>` for a paid purchase, so customers on Free or pay-as-you-go saw the pick-a-plan empty state instead of their actual plan, with no way to switch. It now renders for any active purchase and offers `Upgrade` or `Change plan` based on the catalog, resolved through the new `mergePlanSnapshot` / `findCatalogPlan` helpers so a thin purchase snapshot still matches its catalog plan.
14
+
15
+ A $0 usage-based plan also rendered as `Free`. `<CurrentPlanCard>` now formats a usage rate for metered plans (falling back to the catalog plan when the snapshot has no `options[]`) and omits the price line when no rate can be resolved.
16
+
17
+ UI: the plan card no longer nests a second bordered box inside the MCP card, the surface carries a `Your plan` title in the same slot as checkout's `Choose a plan`, and the price/rate and balance values can be captioned via the new `showFieldLabels` prop (off by default).
4
18
 
5
19
  ### Patch Changes
6
20
 
21
+ - 4771b85: Widen the `@solvapay/mcp-core` peer range to accept `^0.4.0`. Existing installs pinned to `^0.3.0` keep resolving to 0.3.x and are unaffected; moving to mcp-core 0.4.x is now an explicit opt-in that no longer forces a peer conflict.
22
+ - 96fc49f: Stop seller-card email and support URL from navigating the MCP widget iframe. `mailto:` and `target="_blank"` anchors were landing the sandboxed frame on a blocked page with no way back; both rows now render as plain `DetailRow` text, matching the customer-card email.
7
23
  - Updated dependencies [a98faa9]
8
- - @solvapay/core@1.6.0
24
+ - Updated dependencies [0f5db86]
25
+ - Updated dependencies [4d6340a]
26
+ - Updated dependencies [fbaabc8]
27
+ - Updated dependencies [5d37370]
28
+ - @solvapay/core@1.6.0-preview-11dbbe603cd1b9a44cc41b5eee3686bc4000f97f
29
+ - @solvapay/mcp-core@0.4.0-preview-11dbbe603cd1b9a44cc41b5eee3686bc4000f97f
9
30
 
10
31
  ## 2.1.0
11
32
 
@@ -30,6 +30,7 @@ import {
30
30
  useBalance,
31
31
  useCheckoutFlow,
32
32
  useCopy,
33
+ useExternalLinkClick,
33
34
  useLocale,
34
35
  usePlan,
35
36
  usePlanSelection,
@@ -40,7 +41,7 @@ import {
40
41
  useTopupAmountSelector,
41
42
  withPaymentElementDefaults,
42
43
  writeAutoRechargeCache
43
- } from "./chunk-LL6AYSTC.js";
44
+ } from "./chunk-WISTNSUH.js";
44
45
 
45
46
  // src/TopupForm.tsx
46
47
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -3160,9 +3161,10 @@ function Plans({ className, children }) {
3160
3161
  function hidesFreePlan(plan) {
3161
3162
  return plan.requiresPayment !== false;
3162
3163
  }
3163
- var HostedCheckoutLink = forwardRef7(function PaywallNoticeHostedCheckoutLink({ asChild, children, className, ...rest }, forwardedRef) {
3164
+ var HostedCheckoutLink = forwardRef7(function PaywallNoticeHostedCheckoutLink({ asChild, children, className, onClick, ...rest }, forwardedRef) {
3164
3165
  const ctx = usePaywallNoticeCtx("HostedCheckoutLink");
3165
3166
  const copy = useCopy();
3167
+ const handleExternalClick = useExternalLinkClick();
3166
3168
  const href = ctx.content.checkoutUrl;
3167
3169
  if (!href) return null;
3168
3170
  const Comp = asChild ? Slot : "a";
@@ -3175,6 +3177,7 @@ var HostedCheckoutLink = forwardRef7(function PaywallNoticeHostedCheckoutLink({
3175
3177
  rel: "noopener noreferrer",
3176
3178
  "data-solvapay-paywall-hosted-link": "",
3177
3179
  className: className ?? ctx.classNames.hostedLink,
3180
+ onClick: composeEventHandlers(onClick, handleExternalClick),
3178
3181
  ...rest,
3179
3182
  children: children ?? copy.paywall.hostedCheckoutButton
3180
3183
  }
@@ -7,14 +7,19 @@ import {
7
7
  composeEventHandlers,
8
8
  createHttpTransport,
9
9
  createTransportCacheKey,
10
+ formatPaygRate,
10
11
  formatPrice,
11
12
  interpolate,
13
+ useBalance,
12
14
  useCopy,
15
+ useExternalLinkClick,
16
+ useLocale,
17
+ useOpenExternal,
13
18
  usePurchase,
14
19
  usePurchaseStatus,
15
20
  useSolvaPay,
16
21
  useTransport
17
- } from "./chunk-LL6AYSTC.js";
22
+ } from "./chunk-WISTNSUH.js";
18
23
 
19
24
  // src/components/CancelledPlanNotice.tsx
20
25
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -268,6 +273,8 @@ var LaunchCustomerPortalButton = forwardRef(function LaunchCustomerPortalButton2
268
273
  }, forwardedRef) {
269
274
  const { status, url, ensure } = useCustomerSessionUrl();
270
275
  const copy = useCopy();
276
+ const handleExternalClick = useExternalLinkClick();
277
+ const openExternal = useOpenExternal();
271
278
  const [clickState, setClickState] = useState2("idle");
272
279
  const isReady = status === "ready" && typeof url === "string";
273
280
  const label = children ?? copy.customerPortal.launchButton;
@@ -277,19 +284,22 @@ var LaunchCustomerPortalButton = forwardRef(function LaunchCustomerPortalButton2
277
284
  clickState === "pending" ? loadingClassName : null,
278
285
  clickState === "error" ? errorClassName : null
279
286
  ].filter(Boolean).join(" ") || void 0;
280
- const handleReadyClick = () => {
287
+ const handleReadyClick = (event) => {
281
288
  if (!isReady || !url) return;
282
289
  onLaunch?.(url);
290
+ handleExternalClick(event);
283
291
  };
284
292
  const handlePendingClick = async (event) => {
285
293
  event.preventDefault();
286
294
  setClickState("pending");
287
295
  try {
288
296
  const resolved = await ensure();
289
- setClickState("idle");
290
- if (typeof window !== "undefined") {
291
- window.open(resolved, "_blank", "noopener,noreferrer");
297
+ if (!await openExternal(resolved)) {
298
+ setClickState("error");
299
+ onError?.(new Error(`Host declined to open the customer portal: ${resolved}`));
300
+ return;
292
301
  }
302
+ setClickState("idle");
293
303
  onLaunch?.(resolved);
294
304
  } catch (err) {
295
305
  const error = err instanceof Error ? err : new Error(String(err));
@@ -363,7 +373,7 @@ var UpdatePaymentMethodButton = forwardRef2(function UpdatePaymentMethodButton2(
363
373
 
364
374
  // src/components/CurrentPlanCard.tsx
365
375
  import { countsUsage } from "@solvapay/core";
366
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
376
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
367
377
  function PlanTypeLine({
368
378
  purchase,
369
379
  formatDate,
@@ -374,14 +384,7 @@ function PlanTypeLine({
374
384
  if (purchase.isRecurring) {
375
385
  const date2 = formatDate(purchase.nextBillingDate);
376
386
  if (!date2) return null;
377
- return /* @__PURE__ */ jsx5(
378
- "span",
379
- {
380
- className,
381
- "data-solvapay-current-plan-next-billing": "",
382
- children: interpolate(copy.currentPlan.nextBilling, { date: date2 })
383
- }
384
- );
387
+ return /* @__PURE__ */ jsx5("span", { className, "data-solvapay-current-plan-next-billing": "", children: interpolate(copy.currentPlan.nextBilling, { date: date2 }) });
385
388
  }
386
389
  if (isMetered) {
387
390
  return null;
@@ -389,6 +392,62 @@ function PlanTypeLine({
389
392
  const date = formatDate(purchase.endDate);
390
393
  return /* @__PURE__ */ jsx5("span", { className, "data-solvapay-current-plan-expires": "", children: date ? interpolate(copy.currentPlan.expiresOn, { date }) : copy.currentPlan.validIndefinitely });
391
394
  }
395
+ function asPaygPlan(plan) {
396
+ if (!plan) return null;
397
+ return {
398
+ reference: plan.reference ?? void 0,
399
+ name: "name" in plan ? plan.name : void 0,
400
+ price: plan.price ?? void 0,
401
+ currency: plan.currency ?? void 0,
402
+ requiresPayment: "requiresPayment" in plan ? plan.requiresPayment ?? void 0 : void 0,
403
+ // Snapshot `options` are generated as `{ [key: string]: unknown }[]`;
404
+ // formatPaygRate reads them through `@solvapay/core` as PricingOptionLike.
405
+ options: plan.options
406
+ };
407
+ }
408
+ function resolvePriceLabel({
409
+ isUsageBased,
410
+ snapshot,
411
+ catalogPlan,
412
+ amount,
413
+ currency,
414
+ intervalLabel,
415
+ locale,
416
+ balance
417
+ }) {
418
+ if (isUsageBased) {
419
+ const snapshotPlan = asPaygPlan(snapshot);
420
+ const fromSnapshot = snapshotPlan ? formatPaygRate(snapshotPlan, locale, balance) : null;
421
+ if (fromSnapshot) return fromSnapshot;
422
+ const catalogAsPlan = asPaygPlan(catalogPlan);
423
+ if (catalogAsPlan) {
424
+ const fromCatalog = formatPaygRate(catalogAsPlan, locale, balance);
425
+ if (fromCatalog) return fromCatalog;
426
+ }
427
+ return null;
428
+ }
429
+ return formatPrice(amount, currency, {
430
+ interval: intervalLabel
431
+ });
432
+ }
433
+ function PlanField({
434
+ label,
435
+ labelClassName,
436
+ children
437
+ }) {
438
+ if (!label) return /* @__PURE__ */ jsx5(Fragment, { children });
439
+ return /* @__PURE__ */ jsxs4("div", { className: "solvapay-current-plan-field", "data-solvapay-current-plan-field": "", children: [
440
+ /* @__PURE__ */ jsx5(
441
+ "span",
442
+ {
443
+ className: labelClassName ?? "solvapay-current-plan-field-label",
444
+ "data-solvapay-current-plan-field-label": "",
445
+ children: label
446
+ }
447
+ ),
448
+ children
449
+ ] });
450
+ }
392
451
  function PaymentMethodLine({
393
452
  paymentMethod,
394
453
  className
@@ -422,33 +481,45 @@ var CurrentPlanCard = ({
422
481
  hideProductContext,
423
482
  showStartDate,
424
483
  showReference,
484
+ showFieldLabels,
485
+ plans,
425
486
  classNames: overrides,
426
487
  className
427
488
  }) => {
428
489
  const copy = useCopy();
490
+ const locale = useLocale();
491
+ const balance = useBalance();
429
492
  const { activePurchase } = usePurchase();
430
493
  const { formatDate, shouldShowCancelledNotice } = usePurchaseStatus();
431
494
  const { paymentMethod } = usePaymentMethod();
432
495
  if (!activePurchase) return null;
433
- const isUsageBased = countsUsage(activePurchase.planSnapshot) || activePurchase.planSnapshot?.isMetered === true;
496
+ const snapshot = activePurchase.planSnapshot;
497
+ const catalogPlan = plans?.find(
498
+ (p) => p.reference === (snapshot?.reference ?? activePurchase.planRef)
499
+ );
500
+ const isUsageBased = countsUsage(snapshot) || snapshot?.isMetered === true || countsUsage(catalogPlan);
434
501
  const planType = activePurchase.isRecurring ? "recurring" : isUsageBased ? "usage-based" : "one-time";
435
502
  const amount = activePurchase.originalAmount ?? activePurchase.amount ?? 0;
436
503
  const currency = activePurchase.currency ?? "usd";
437
504
  const rawCycle = activePurchase.billingCycle;
438
505
  const cycleKey = rawCycle && rawCycle in copy.currentPlan.cycleUnit ? rawCycle : void 0;
439
506
  const intervalLabel = cycleKey ? copy.currentPlan.cycleUnit[cycleKey] ?? rawCycle : rawCycle;
440
- const priceLabel = formatPrice(amount, currency, {
441
- interval: intervalLabel
507
+ const priceLabel = resolvePriceLabel({
508
+ isUsageBased,
509
+ snapshot,
510
+ catalogPlan,
511
+ amount,
512
+ currency,
513
+ intervalLabel,
514
+ locale,
515
+ balance
442
516
  });
443
517
  const planName = activePurchase.planSnapshot?.name ?? activePurchase.productName;
444
518
  const productContext = activePurchase.productName && activePurchase.productName !== planName ? activePurchase.productName : null;
445
- const rootClass = [
446
- "solvapay-current-plan-card",
447
- overrides?.root,
448
- className
449
- ].filter(Boolean).join(" ");
519
+ const rootClass = ["solvapay-current-plan-card", overrides?.root, className].filter(Boolean).join(" ");
450
520
  const shouldShowPaymentMethod = !hidePaymentMethod && paymentMethod !== null;
451
521
  const showCancelButton = !hideCancelButton && !activePurchase.cancelledAt && !shouldShowCancelledNotice;
522
+ const showActions = !hideUpdatePaymentButton || showCancelButton;
452
523
  return /* @__PURE__ */ jsxs4(
453
524
  "section",
454
525
  {
@@ -481,14 +552,21 @@ var CurrentPlanCard = ({
481
552
  children: planName
482
553
  }
483
554
  ),
484
- /* @__PURE__ */ jsx5(
485
- "p",
555
+ priceLabel ? /* @__PURE__ */ jsx5(
556
+ PlanField,
486
557
  {
487
- className: overrides?.price ?? "solvapay-current-plan-price",
488
- "data-solvapay-current-plan-price": "",
489
- children: priceLabel
558
+ label: showFieldLabels ? isUsageBased ? copy.currentPlan.rateFieldLabel : copy.currentPlan.priceFieldLabel : null,
559
+ labelClassName: overrides?.fieldLabel,
560
+ children: /* @__PURE__ */ jsx5(
561
+ "p",
562
+ {
563
+ className: overrides?.price ?? "solvapay-current-plan-price",
564
+ "data-solvapay-current-plan-price": "",
565
+ children: priceLabel
566
+ }
567
+ )
490
568
  }
491
- ),
569
+ ) : null,
492
570
  /* @__PURE__ */ jsx5(
493
571
  PlanTypeLine,
494
572
  {
@@ -497,6 +575,35 @@ var CurrentPlanCard = ({
497
575
  className: overrides?.dateLine ?? "solvapay-current-plan-date-line"
498
576
  }
499
577
  ),
578
+ isUsageBased && activePurchase.isRecurring && !hideUsageMeter && /* @__PURE__ */ jsx5(
579
+ "section",
580
+ {
581
+ className: overrides?.usageMeter ?? "solvapay-current-plan-usage-meter",
582
+ "data-solvapay-current-plan-usage-meter": "",
583
+ children: /* @__PURE__ */ jsxs4(UsageMeter.Root, { children: [
584
+ /* @__PURE__ */ jsx5(UsageMeter.Label, {}),
585
+ /* @__PURE__ */ jsx5(UsageMeter.Bar, {}),
586
+ /* @__PURE__ */ jsx5(UsageMeter.Percentage, {}),
587
+ /* @__PURE__ */ jsx5(UsageMeter.ResetsIn, {}),
588
+ /* @__PURE__ */ jsx5(UsageMeter.Loading, {})
589
+ ] })
590
+ }
591
+ ),
592
+ isUsageBased && /* @__PURE__ */ jsx5(
593
+ PlanField,
594
+ {
595
+ label: showFieldLabels ? copy.currentPlan.balanceFieldLabel : null,
596
+ labelClassName: overrides?.fieldLabel,
597
+ children: /* @__PURE__ */ jsx5(
598
+ "p",
599
+ {
600
+ className: overrides?.balanceLine ?? "solvapay-current-plan-balance-line",
601
+ "data-solvapay-current-plan-balance-line": "",
602
+ children: /* @__PURE__ */ jsx5(BalanceBadge, {})
603
+ }
604
+ )
605
+ }
606
+ ),
500
607
  showStartDate && (() => {
501
608
  const started = formatDate(activePurchase.startDate);
502
609
  if (!started) return null;
@@ -517,28 +624,6 @@ var CurrentPlanCard = ({
517
624
  children: activePurchase.reference
518
625
  }
519
626
  ),
520
- isUsageBased && activePurchase.isRecurring && !hideUsageMeter && /* @__PURE__ */ jsx5(
521
- "section",
522
- {
523
- className: overrides?.usageMeter ?? "solvapay-current-plan-usage-meter",
524
- "data-solvapay-current-plan-usage-meter": "",
525
- children: /* @__PURE__ */ jsxs4(UsageMeter.Root, { children: [
526
- /* @__PURE__ */ jsx5(UsageMeter.Label, {}),
527
- /* @__PURE__ */ jsx5(UsageMeter.Bar, {}),
528
- /* @__PURE__ */ jsx5(UsageMeter.Percentage, {}),
529
- /* @__PURE__ */ jsx5(UsageMeter.ResetsIn, {}),
530
- /* @__PURE__ */ jsx5(UsageMeter.Loading, {})
531
- ] })
532
- }
533
- ),
534
- isUsageBased && /* @__PURE__ */ jsx5(
535
- "p",
536
- {
537
- className: overrides?.balanceLine ?? "solvapay-current-plan-balance-line",
538
- "data-solvapay-current-plan-balance-line": "",
539
- children: /* @__PURE__ */ jsx5(BalanceBadge, {})
540
- }
541
- ),
542
627
  shouldShowPaymentMethod && paymentMethod && /* @__PURE__ */ jsx5(
543
628
  PaymentMethodLine,
544
629
  {
@@ -546,7 +631,7 @@ var CurrentPlanCard = ({
546
631
  className: overrides?.paymentMethod ?? "solvapay-current-plan-payment-method"
547
632
  }
548
633
  ),
549
- /* @__PURE__ */ jsxs4(
634
+ showActions && /* @__PURE__ */ jsxs4(
550
635
  "section",
551
636
  {
552
637
  className: overrides?.actions ?? "solvapay-current-plan-actions",