@solvapay/server 2.3.1 → 2.4.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/dist/edge.d.ts +50 -17
- package/dist/edge.js +88 -14
- package/dist/fetch/index.cjs +82 -9
- package/dist/fetch/index.js +82 -9
- package/dist/index.cjs +140 -28
- package/dist/index.d.cts +98 -18
- package/dist/index.d.ts +98 -18
- package/dist/index.js +139 -28
- package/package.json +2 -2
package/dist/edge.d.ts
CHANGED
|
@@ -4085,7 +4085,36 @@ interface PaywallMetadata {
|
|
|
4085
4085
|
/**
|
|
4086
4086
|
* Structured content for paywall errors (MCP structuredContent and manual handling).
|
|
4087
4087
|
*/
|
|
4088
|
-
|
|
4088
|
+
/**
|
|
4089
|
+
* Fields shared by every `PaywallStructuredContent` kind so a text-only
|
|
4090
|
+
* host can recover from a gate without a second tool call. Every field
|
|
4091
|
+
* other than `kind` / `product` / `checkoutUrl` / `message` is omitted
|
|
4092
|
+
* when the backend did not send it — never defaulted.
|
|
4093
|
+
*/
|
|
4094
|
+
type PaywallGateRecoveryFields = {
|
|
4095
|
+
/** Active plan reference from `limits.plan`. */
|
|
4096
|
+
planRef?: string;
|
|
4097
|
+
/** Product plans from `checkLimits` (activation already had this). */
|
|
4098
|
+
plans?: LimitPlanSummary[];
|
|
4099
|
+
/** Meter the gated call was charged against. */
|
|
4100
|
+
meterName?: string;
|
|
4101
|
+
/** Per-unit charge of the active plan, in minor currency units. */
|
|
4102
|
+
unitPriceMinor?: number;
|
|
4103
|
+
/** ISO 4217 currency for `unitPriceMinor`. */
|
|
4104
|
+
currency?: string;
|
|
4105
|
+
/**
|
|
4106
|
+
* Included-usage counters. Omitted when `freeUnits` is absent or `0`
|
|
4107
|
+
* (the unlimited sentinel) so we never emit misleading zeros.
|
|
4108
|
+
*/
|
|
4109
|
+
included?: {
|
|
4110
|
+
total: number;
|
|
4111
|
+
used: number;
|
|
4112
|
+
remaining: number;
|
|
4113
|
+
};
|
|
4114
|
+
/** Prepaid credit balance, coalesced from nested or top-level fields. */
|
|
4115
|
+
creditBalance?: number;
|
|
4116
|
+
};
|
|
4117
|
+
type PaywallStructuredContent = ({
|
|
4089
4118
|
kind: 'payment_required';
|
|
4090
4119
|
/** Product ref from paywall metadata (or env default) */
|
|
4091
4120
|
product: string;
|
|
@@ -4100,7 +4129,7 @@ type PaywallStructuredContent = {
|
|
|
4100
4129
|
balance?: LimitActivationBalance;
|
|
4101
4130
|
/** Rich product context from checkLimits (name, ref, provider slug/id) */
|
|
4102
4131
|
productDetails?: LimitActivationProduct;
|
|
4103
|
-
} | {
|
|
4132
|
+
} & PaywallGateRecoveryFields) | ({
|
|
4104
4133
|
kind: 'activation_required';
|
|
4105
4134
|
/** Product ref from paywall metadata (or env default) */
|
|
4106
4135
|
product: string;
|
|
@@ -4110,11 +4139,10 @@ type PaywallStructuredContent = {
|
|
|
4110
4139
|
*/
|
|
4111
4140
|
checkoutUrl: string;
|
|
4112
4141
|
confirmationUrl?: string;
|
|
4113
|
-
plans?: LimitPlanSummary[];
|
|
4114
4142
|
balance?: LimitActivationBalance;
|
|
4115
4143
|
/** Rich product context from checkLimits (name, ref, provider slug/id) */
|
|
4116
4144
|
productDetails?: LimitActivationProduct;
|
|
4117
|
-
};
|
|
4145
|
+
} & PaywallGateRecoveryFields);
|
|
4118
4146
|
/**
|
|
4119
4147
|
* Runtime type guard for `PaywallStructuredContent`.
|
|
4120
4148
|
*
|
|
@@ -5281,6 +5309,8 @@ type PaywallState = {
|
|
|
5281
5309
|
kind: 'topup_required';
|
|
5282
5310
|
} | {
|
|
5283
5311
|
kind: 'upgrade_required';
|
|
5312
|
+
} | {
|
|
5313
|
+
kind: 'limit_reached';
|
|
5284
5314
|
} | {
|
|
5285
5315
|
kind: 'reactivation_required';
|
|
5286
5316
|
};
|
|
@@ -5302,10 +5332,12 @@ type PaywallState = {
|
|
|
5302
5332
|
* `creditBalance === 0` field, or `remaining === 0` as a
|
|
5303
5333
|
* fallback for older backend responses that omit both credit
|
|
5304
5334
|
* fields on usage-based plans.
|
|
5305
|
-
* 4.
|
|
5335
|
+
* 4. Active plan at included cap (`plan` is a non-empty ref and
|
|
5336
|
+
* `remaining <= 0`) → `limit_reached`. This is the "on Free, used
|
|
5337
|
+
* 3 of 3" case — not "no plan".
|
|
5338
|
+
* 5. Everything else → `upgrade_required`, including:
|
|
5306
5339
|
* - `limits === null` (defensive),
|
|
5307
|
-
* - no active plan on the product
|
|
5308
|
-
* - recurring plan at period cap (`remaining <= 0`).
|
|
5340
|
+
* - no active plan on the product.
|
|
5309
5341
|
*
|
|
5310
5342
|
* `reactivation_required` is deferred — it needs a distinct backend
|
|
5311
5343
|
* signal (future `LimitResponse.inactivePurchaseRef`) which isn't
|
|
@@ -5315,12 +5347,13 @@ type PaywallState = {
|
|
|
5315
5347
|
*/
|
|
5316
5348
|
declare function classifyPaywallState(limits: LimitResponseWithPlan | null): PaywallState;
|
|
5317
5349
|
/**
|
|
5318
|
-
* Produce the terminal-friendly gate message. Names
|
|
5319
|
-
*
|
|
5320
|
-
*
|
|
5321
|
-
*
|
|
5322
|
-
*
|
|
5323
|
-
*
|
|
5350
|
+
* Produce the terminal-friendly gate message. Names the `account`
|
|
5351
|
+
* viewer (with a `view` hint when the landing screen matters) or
|
|
5352
|
+
* `activate_plan` when a specific plan needs activating. Inlines
|
|
5353
|
+
* `gate.checkoutUrl` when present so terminal-only MCP hosts (Claude
|
|
5354
|
+
* Code, CLI clients) can open a browser directly. States the
|
|
5355
|
+
* 15-minute session lifetime inline — the URL is a bearer credential,
|
|
5356
|
+
* not a durable link.
|
|
5324
5357
|
*
|
|
5325
5358
|
* Kept as a pure string so the adapter layer can concatenate it with
|
|
5326
5359
|
* an optional narrator prefix without parsing structured copy.
|
|
@@ -5333,10 +5366,10 @@ declare function buildGateMessage(state: PaywallState, gate: PaywallStructuredCo
|
|
|
5333
5366
|
* like a softer version of the same text-only nudge path.
|
|
5334
5367
|
*
|
|
5335
5368
|
* Receives the `PaywallState` the classifier would have produced if
|
|
5336
|
-
* the customer had tripped the gate. `upgrade_required
|
|
5337
|
-
* `topup_required` are the
|
|
5338
|
-
* copy; the others are no-ops (shouldn't happen —
|
|
5339
|
-
* on successful calls).
|
|
5369
|
+
* the customer had tripped the gate. `upgrade_required`,
|
|
5370
|
+
* `limit_reached`, and `topup_required` are the kinds that currently
|
|
5371
|
+
* produce nudge copy; the others are no-ops (shouldn't happen —
|
|
5372
|
+
* nudges only fire on successful calls).
|
|
5340
5373
|
*/
|
|
5341
5374
|
declare function buildNudgeMessage(state: PaywallState, limits: LimitResponseWithPlan | null): string;
|
|
5342
5375
|
|
package/dist/edge.js
CHANGED
|
@@ -804,6 +804,8 @@ async function verifyProductConfiguration(options) {
|
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
// src/paywall-state.ts
|
|
807
|
+
var CHECKOUT_SESSION_TTL_MINUTES = 15;
|
|
808
|
+
var DOCS_HINT = "See docs://solvapay/overview.md.";
|
|
807
809
|
function classifyPaywallState(limits) {
|
|
808
810
|
if (!limits) return { kind: "upgrade_required" };
|
|
809
811
|
if (limits.activationRequired === true) {
|
|
@@ -824,34 +826,77 @@ function classifyPaywallState(limits) {
|
|
|
824
826
|
return { kind: "topup_required" };
|
|
825
827
|
}
|
|
826
828
|
}
|
|
829
|
+
if (limits.plan.length > 0 && limits.remaining <= 0) {
|
|
830
|
+
return { kind: "limit_reached" };
|
|
831
|
+
}
|
|
827
832
|
return { kind: "upgrade_required" };
|
|
828
833
|
}
|
|
834
|
+
function formatMinor(amountMinor, currency) {
|
|
835
|
+
const major = amountMinor / 100;
|
|
836
|
+
try {
|
|
837
|
+
return new Intl.NumberFormat("en-US", {
|
|
838
|
+
style: "currency",
|
|
839
|
+
currency: currency.toUpperCase(),
|
|
840
|
+
minimumFractionDigits: 2,
|
|
841
|
+
maximumFractionDigits: 2
|
|
842
|
+
}).format(major);
|
|
843
|
+
} catch {
|
|
844
|
+
return `${currency.toUpperCase()} ${major.toFixed(2)}`;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function meterLabel(gate) {
|
|
848
|
+
if (!gate.meterName) return "units";
|
|
849
|
+
return gate.meterName.replace(/_/g, " ");
|
|
850
|
+
}
|
|
851
|
+
function namedCheckoutMarkdown(url) {
|
|
852
|
+
return `[Open checkout](${url})`;
|
|
853
|
+
}
|
|
854
|
+
var VIEWER_TOOL = "account";
|
|
855
|
+
function callViewer(view) {
|
|
856
|
+
return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
|
|
857
|
+
}
|
|
858
|
+
function recoverClause(url, verb, view) {
|
|
859
|
+
if (url) {
|
|
860
|
+
return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
|
|
861
|
+
}
|
|
862
|
+
return ` ${callViewer(view).replace(/^c/, "C")}.`;
|
|
863
|
+
}
|
|
829
864
|
function buildGateMessage(state, gate) {
|
|
830
865
|
const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
|
|
831
|
-
const openClause = url ? `, or open ${url} in a browser` : "";
|
|
832
866
|
switch (state.kind) {
|
|
867
|
+
case "limit_reached": {
|
|
868
|
+
const included = gate.included;
|
|
869
|
+
const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
|
|
870
|
+
const usedLine = included ? `You've used ${included.used} of ${included.total} included ${meterLabel(gate)} this period.` : `You've reached the included usage for this period.`;
|
|
871
|
+
const nextLine = price ? ` The next call is ${price}.` : "";
|
|
872
|
+
return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
|
|
873
|
+
}
|
|
833
874
|
case "activation_required":
|
|
834
|
-
return `Your plan needs activation
|
|
835
|
-
case "topup_required":
|
|
836
|
-
|
|
875
|
+
return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
|
|
876
|
+
case "topup_required": {
|
|
877
|
+
const currency = gate.currency ?? gate.balance?.currency ?? "USD";
|
|
878
|
+
const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
|
|
879
|
+
return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
|
|
880
|
+
}
|
|
837
881
|
case "upgrade_required":
|
|
838
|
-
return `You don't have an active plan for this tool
|
|
882
|
+
return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
|
|
839
883
|
case "reactivation_required":
|
|
840
|
-
return `Your previous plan is no longer active.
|
|
884
|
+
return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
|
|
841
885
|
}
|
|
842
886
|
}
|
|
843
887
|
function buildNudgeMessage(state, limits) {
|
|
844
888
|
const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
|
|
845
|
-
const visitClause = url ? `, or
|
|
889
|
+
const visitClause = url ? `, or ${namedCheckoutMarkdown(url)}` : "";
|
|
846
890
|
switch (state.kind) {
|
|
847
891
|
case "topup_required":
|
|
848
|
-
return `Heads up \u2014 running low on credits.
|
|
892
|
+
return `Heads up \u2014 running low on credits. ${callViewer("topup").replace(/^c/, "C")} to add more${visitClause}.`;
|
|
849
893
|
case "upgrade_required":
|
|
850
|
-
|
|
894
|
+
case "limit_reached":
|
|
895
|
+
return `Heads up \u2014 approaching your plan's limit this period. ${callViewer("checkout").replace(/^c/, "C")} for more headroom${visitClause}.`;
|
|
851
896
|
case "activation_required":
|
|
852
|
-
return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool
|
|
897
|
+
return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool with a \`planRef\`${visitClause}.`;
|
|
853
898
|
case "reactivation_required":
|
|
854
|
-
return `Heads up \u2014 your plan is no longer active.
|
|
899
|
+
return `Heads up \u2014 your plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it${visitClause}.`;
|
|
855
900
|
}
|
|
856
901
|
}
|
|
857
902
|
|
|
@@ -862,26 +907,55 @@ function allPaidPlansArePayg(plans) {
|
|
|
862
907
|
if (paidPlans.length === 0) return false;
|
|
863
908
|
return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
|
|
864
909
|
}
|
|
910
|
+
function activePlanOf(limits) {
|
|
911
|
+
if (!limits.plan) return void 0;
|
|
912
|
+
return limits.plans?.find((p) => p.reference === limits.plan);
|
|
913
|
+
}
|
|
914
|
+
function includedFromLimits(limits) {
|
|
915
|
+
const total = activePlanOf(limits)?.freeUnits;
|
|
916
|
+
if (total == null || total === 0) return void 0;
|
|
917
|
+
const remaining = limits.remaining;
|
|
918
|
+
return {
|
|
919
|
+
total,
|
|
920
|
+
used: Math.max(0, total - remaining),
|
|
921
|
+
remaining
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function recoveryFields(limits) {
|
|
925
|
+
const plan = activePlanOf(limits);
|
|
926
|
+
const included = includedFromLimits(limits);
|
|
927
|
+
const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
|
|
928
|
+
return {
|
|
929
|
+
...limits.plan ? { planRef: limits.plan } : {},
|
|
930
|
+
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
931
|
+
...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
|
|
932
|
+
...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
|
|
933
|
+
...included ? { included } : {},
|
|
934
|
+
...creditBalance !== void 0 ? { creditBalance } : {}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
865
937
|
function buildPaywallGate(productRef, limits) {
|
|
866
938
|
const checkoutUrl = limits.checkoutUrl;
|
|
867
939
|
const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
|
|
868
940
|
const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
|
|
941
|
+
const recovery = recoveryFields(limits);
|
|
869
942
|
const preMessage = limits.activationRequired || useActivationForTopup ? {
|
|
870
943
|
kind: "activation_required",
|
|
871
944
|
product: productRef,
|
|
872
945
|
message: "",
|
|
873
946
|
checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
|
|
874
947
|
...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
|
|
875
|
-
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
876
948
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
877
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
949
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
950
|
+
...recovery
|
|
878
951
|
} : {
|
|
879
952
|
kind: "payment_required",
|
|
880
953
|
product: productRef,
|
|
881
954
|
checkoutUrl: checkoutUrl || "",
|
|
882
955
|
message: "",
|
|
883
956
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
884
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
957
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
958
|
+
...recovery
|
|
885
959
|
};
|
|
886
960
|
return { ...preMessage, message: buildGateMessage(state, preMessage) };
|
|
887
961
|
}
|
package/dist/fetch/index.cjs
CHANGED
|
@@ -965,6 +965,8 @@ function createSolvaPayClient(opts) {
|
|
|
965
965
|
}
|
|
966
966
|
|
|
967
967
|
// src/paywall-state.ts
|
|
968
|
+
var CHECKOUT_SESSION_TTL_MINUTES = 15;
|
|
969
|
+
var DOCS_HINT = "See docs://solvapay/overview.md.";
|
|
968
970
|
function classifyPaywallState(limits) {
|
|
969
971
|
if (!limits) return { kind: "upgrade_required" };
|
|
970
972
|
if (limits.activationRequired === true) {
|
|
@@ -985,20 +987,62 @@ function classifyPaywallState(limits) {
|
|
|
985
987
|
return { kind: "topup_required" };
|
|
986
988
|
}
|
|
987
989
|
}
|
|
990
|
+
if (limits.plan.length > 0 && limits.remaining <= 0) {
|
|
991
|
+
return { kind: "limit_reached" };
|
|
992
|
+
}
|
|
988
993
|
return { kind: "upgrade_required" };
|
|
989
994
|
}
|
|
995
|
+
function formatMinor(amountMinor, currency) {
|
|
996
|
+
const major = amountMinor / 100;
|
|
997
|
+
try {
|
|
998
|
+
return new Intl.NumberFormat("en-US", {
|
|
999
|
+
style: "currency",
|
|
1000
|
+
currency: currency.toUpperCase(),
|
|
1001
|
+
minimumFractionDigits: 2,
|
|
1002
|
+
maximumFractionDigits: 2
|
|
1003
|
+
}).format(major);
|
|
1004
|
+
} catch {
|
|
1005
|
+
return `${currency.toUpperCase()} ${major.toFixed(2)}`;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function meterLabel(gate) {
|
|
1009
|
+
if (!gate.meterName) return "units";
|
|
1010
|
+
return gate.meterName.replace(/_/g, " ");
|
|
1011
|
+
}
|
|
1012
|
+
function namedCheckoutMarkdown(url) {
|
|
1013
|
+
return `[Open checkout](${url})`;
|
|
1014
|
+
}
|
|
1015
|
+
var VIEWER_TOOL = "account";
|
|
1016
|
+
function callViewer(view) {
|
|
1017
|
+
return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
|
|
1018
|
+
}
|
|
1019
|
+
function recoverClause(url, verb, view) {
|
|
1020
|
+
if (url) {
|
|
1021
|
+
return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
|
|
1022
|
+
}
|
|
1023
|
+
return ` ${callViewer(view).replace(/^c/, "C")}.`;
|
|
1024
|
+
}
|
|
990
1025
|
function buildGateMessage(state, gate) {
|
|
991
1026
|
const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
|
|
992
|
-
const openClause = url ? `, or open ${url} in a browser` : "";
|
|
993
1027
|
switch (state.kind) {
|
|
1028
|
+
case "limit_reached": {
|
|
1029
|
+
const included = gate.included;
|
|
1030
|
+
const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
|
|
1031
|
+
const usedLine = included ? `You've used ${included.used} of ${included.total} included ${meterLabel(gate)} this period.` : `You've reached the included usage for this period.`;
|
|
1032
|
+
const nextLine = price ? ` The next call is ${price}.` : "";
|
|
1033
|
+
return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
|
|
1034
|
+
}
|
|
994
1035
|
case "activation_required":
|
|
995
|
-
return `Your plan needs activation
|
|
996
|
-
case "topup_required":
|
|
997
|
-
|
|
1036
|
+
return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
|
|
1037
|
+
case "topup_required": {
|
|
1038
|
+
const currency = gate.currency ?? gate.balance?.currency ?? "USD";
|
|
1039
|
+
const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
|
|
1040
|
+
return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
|
|
1041
|
+
}
|
|
998
1042
|
case "upgrade_required":
|
|
999
|
-
return `You don't have an active plan for this tool
|
|
1043
|
+
return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
|
|
1000
1044
|
case "reactivation_required":
|
|
1001
|
-
return `Your previous plan is no longer active.
|
|
1045
|
+
return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
|
|
1002
1046
|
}
|
|
1003
1047
|
}
|
|
1004
1048
|
|
|
@@ -1009,26 +1053,55 @@ function allPaidPlansArePayg(plans) {
|
|
|
1009
1053
|
if (paidPlans.length === 0) return false;
|
|
1010
1054
|
return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
|
|
1011
1055
|
}
|
|
1056
|
+
function activePlanOf(limits) {
|
|
1057
|
+
if (!limits.plan) return void 0;
|
|
1058
|
+
return limits.plans?.find((p) => p.reference === limits.plan);
|
|
1059
|
+
}
|
|
1060
|
+
function includedFromLimits(limits) {
|
|
1061
|
+
const total = activePlanOf(limits)?.freeUnits;
|
|
1062
|
+
if (total == null || total === 0) return void 0;
|
|
1063
|
+
const remaining = limits.remaining;
|
|
1064
|
+
return {
|
|
1065
|
+
total,
|
|
1066
|
+
used: Math.max(0, total - remaining),
|
|
1067
|
+
remaining
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function recoveryFields(limits) {
|
|
1071
|
+
const plan = activePlanOf(limits);
|
|
1072
|
+
const included = includedFromLimits(limits);
|
|
1073
|
+
const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
|
|
1074
|
+
return {
|
|
1075
|
+
...limits.plan ? { planRef: limits.plan } : {},
|
|
1076
|
+
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
1077
|
+
...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
|
|
1078
|
+
...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
|
|
1079
|
+
...included ? { included } : {},
|
|
1080
|
+
...creditBalance !== void 0 ? { creditBalance } : {}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1012
1083
|
function buildPaywallGate(productRef, limits) {
|
|
1013
1084
|
const checkoutUrl = limits.checkoutUrl;
|
|
1014
1085
|
const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
|
|
1015
1086
|
const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
|
|
1087
|
+
const recovery = recoveryFields(limits);
|
|
1016
1088
|
const preMessage = limits.activationRequired || useActivationForTopup ? {
|
|
1017
1089
|
kind: "activation_required",
|
|
1018
1090
|
product: productRef,
|
|
1019
1091
|
message: "",
|
|
1020
1092
|
checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
|
|
1021
1093
|
...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
|
|
1022
|
-
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
1023
1094
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
1024
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
1095
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
1096
|
+
...recovery
|
|
1025
1097
|
} : {
|
|
1026
1098
|
kind: "payment_required",
|
|
1027
1099
|
product: productRef,
|
|
1028
1100
|
checkoutUrl: checkoutUrl || "",
|
|
1029
1101
|
message: "",
|
|
1030
1102
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
1031
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
1103
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
1104
|
+
...recovery
|
|
1032
1105
|
};
|
|
1033
1106
|
return { ...preMessage, message: buildGateMessage(state, preMessage) };
|
|
1034
1107
|
}
|
package/dist/fetch/index.js
CHANGED
|
@@ -912,6 +912,8 @@ function createSolvaPayClient(opts) {
|
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
// src/paywall-state.ts
|
|
915
|
+
var CHECKOUT_SESSION_TTL_MINUTES = 15;
|
|
916
|
+
var DOCS_HINT = "See docs://solvapay/overview.md.";
|
|
915
917
|
function classifyPaywallState(limits) {
|
|
916
918
|
if (!limits) return { kind: "upgrade_required" };
|
|
917
919
|
if (limits.activationRequired === true) {
|
|
@@ -932,20 +934,62 @@ function classifyPaywallState(limits) {
|
|
|
932
934
|
return { kind: "topup_required" };
|
|
933
935
|
}
|
|
934
936
|
}
|
|
937
|
+
if (limits.plan.length > 0 && limits.remaining <= 0) {
|
|
938
|
+
return { kind: "limit_reached" };
|
|
939
|
+
}
|
|
935
940
|
return { kind: "upgrade_required" };
|
|
936
941
|
}
|
|
942
|
+
function formatMinor(amountMinor, currency) {
|
|
943
|
+
const major = amountMinor / 100;
|
|
944
|
+
try {
|
|
945
|
+
return new Intl.NumberFormat("en-US", {
|
|
946
|
+
style: "currency",
|
|
947
|
+
currency: currency.toUpperCase(),
|
|
948
|
+
minimumFractionDigits: 2,
|
|
949
|
+
maximumFractionDigits: 2
|
|
950
|
+
}).format(major);
|
|
951
|
+
} catch {
|
|
952
|
+
return `${currency.toUpperCase()} ${major.toFixed(2)}`;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function meterLabel(gate) {
|
|
956
|
+
if (!gate.meterName) return "units";
|
|
957
|
+
return gate.meterName.replace(/_/g, " ");
|
|
958
|
+
}
|
|
959
|
+
function namedCheckoutMarkdown(url) {
|
|
960
|
+
return `[Open checkout](${url})`;
|
|
961
|
+
}
|
|
962
|
+
var VIEWER_TOOL = "account";
|
|
963
|
+
function callViewer(view) {
|
|
964
|
+
return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
|
|
965
|
+
}
|
|
966
|
+
function recoverClause(url, verb, view) {
|
|
967
|
+
if (url) {
|
|
968
|
+
return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
|
|
969
|
+
}
|
|
970
|
+
return ` ${callViewer(view).replace(/^c/, "C")}.`;
|
|
971
|
+
}
|
|
937
972
|
function buildGateMessage(state, gate) {
|
|
938
973
|
const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
|
|
939
|
-
const openClause = url ? `, or open ${url} in a browser` : "";
|
|
940
974
|
switch (state.kind) {
|
|
975
|
+
case "limit_reached": {
|
|
976
|
+
const included = gate.included;
|
|
977
|
+
const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
|
|
978
|
+
const usedLine = included ? `You've used ${included.used} of ${included.total} included ${meterLabel(gate)} this period.` : `You've reached the included usage for this period.`;
|
|
979
|
+
const nextLine = price ? ` The next call is ${price}.` : "";
|
|
980
|
+
return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
|
|
981
|
+
}
|
|
941
982
|
case "activation_required":
|
|
942
|
-
return `Your plan needs activation
|
|
943
|
-
case "topup_required":
|
|
944
|
-
|
|
983
|
+
return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
|
|
984
|
+
case "topup_required": {
|
|
985
|
+
const currency = gate.currency ?? gate.balance?.currency ?? "USD";
|
|
986
|
+
const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
|
|
987
|
+
return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
|
|
988
|
+
}
|
|
945
989
|
case "upgrade_required":
|
|
946
|
-
return `You don't have an active plan for this tool
|
|
990
|
+
return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
|
|
947
991
|
case "reactivation_required":
|
|
948
|
-
return `Your previous plan is no longer active.
|
|
992
|
+
return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
|
|
949
993
|
}
|
|
950
994
|
}
|
|
951
995
|
|
|
@@ -956,26 +1000,55 @@ function allPaidPlansArePayg(plans) {
|
|
|
956
1000
|
if (paidPlans.length === 0) return false;
|
|
957
1001
|
return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
|
|
958
1002
|
}
|
|
1003
|
+
function activePlanOf(limits) {
|
|
1004
|
+
if (!limits.plan) return void 0;
|
|
1005
|
+
return limits.plans?.find((p) => p.reference === limits.plan);
|
|
1006
|
+
}
|
|
1007
|
+
function includedFromLimits(limits) {
|
|
1008
|
+
const total = activePlanOf(limits)?.freeUnits;
|
|
1009
|
+
if (total == null || total === 0) return void 0;
|
|
1010
|
+
const remaining = limits.remaining;
|
|
1011
|
+
return {
|
|
1012
|
+
total,
|
|
1013
|
+
used: Math.max(0, total - remaining),
|
|
1014
|
+
remaining
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function recoveryFields(limits) {
|
|
1018
|
+
const plan = activePlanOf(limits);
|
|
1019
|
+
const included = includedFromLimits(limits);
|
|
1020
|
+
const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
|
|
1021
|
+
return {
|
|
1022
|
+
...limits.plan ? { planRef: limits.plan } : {},
|
|
1023
|
+
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
1024
|
+
...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
|
|
1025
|
+
...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
|
|
1026
|
+
...included ? { included } : {},
|
|
1027
|
+
...creditBalance !== void 0 ? { creditBalance } : {}
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
959
1030
|
function buildPaywallGate(productRef, limits) {
|
|
960
1031
|
const checkoutUrl = limits.checkoutUrl;
|
|
961
1032
|
const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
|
|
962
1033
|
const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
|
|
1034
|
+
const recovery = recoveryFields(limits);
|
|
963
1035
|
const preMessage = limits.activationRequired || useActivationForTopup ? {
|
|
964
1036
|
kind: "activation_required",
|
|
965
1037
|
product: productRef,
|
|
966
1038
|
message: "",
|
|
967
1039
|
checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
|
|
968
1040
|
...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
|
|
969
|
-
...limits.plans !== void 0 ? { plans: limits.plans } : {},
|
|
970
1041
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
971
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
1042
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
1043
|
+
...recovery
|
|
972
1044
|
} : {
|
|
973
1045
|
kind: "payment_required",
|
|
974
1046
|
product: productRef,
|
|
975
1047
|
checkoutUrl: checkoutUrl || "",
|
|
976
1048
|
message: "",
|
|
977
1049
|
...limits.balance !== void 0 ? { balance: limits.balance } : {},
|
|
978
|
-
...limits.product !== void 0 ? { productDetails: limits.product } : {}
|
|
1050
|
+
...limits.product !== void 0 ? { productDetails: limits.product } : {},
|
|
1051
|
+
...recovery
|
|
979
1052
|
};
|
|
980
1053
|
return { ...preMessage, message: buildGateMessage(state, preMessage) };
|
|
981
1054
|
}
|