@comfyorg/account-core 1.0.0-alpha.0 → 1.0.0-alpha.2
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/core/billing/billingContracts.d.ts +29 -14
- package/dist/core/billing/billingErrorBody.d.ts +8 -4
- package/dist/core/billing/billingErrorBody.js +14 -4
- package/dist/core/billing/credentialedTransport.d.ts +23 -7
- package/dist/core/billing/credentialedTransport.js +72 -8
- package/dist/core/billing/events.d.ts +47 -0
- package/dist/core/billing/events.js +43 -0
- package/dist/core/billing/index.d.ts +16 -11
- package/dist/core/billing/index.js +8 -6
- package/dist/core/billing/operationLifecycle.d.ts +34 -1
- package/dist/core/billing/operationLifecycle.js +146 -67
- package/dist/core/billing/operationPointer.d.ts +27 -2
- package/dist/core/billing/operationPointer.js +36 -25
- package/dist/core/billing/operationPolicy.d.ts +44 -7
- package/dist/core/billing/operationPolicy.js +64 -3
- package/dist/core/billing/operationState.d.ts +225 -19
- package/dist/core/billing/operationState.js +93 -8
- package/dist/core/billing/paymentCopy.d.ts +6 -1
- package/dist/core/billing/paymentCopy.js +34 -1
- package/dist/core/billing/paymentProjection.d.ts +14 -2
- package/dist/core/billing/paymentProjection.js +37 -9
- package/dist/core/billing/scopedReader.d.ts +11 -5
- package/dist/core/billing/scopedReader.js +20 -9
- package/dist/core/billing/sharedRead.js +4 -2
- package/dist/core/billing/subscriptionCommands.d.ts +63 -9
- package/dist/core/billing/subscriptionCommands.js +46 -30
- package/dist/core/billing/topup.d.ts +26 -0
- package/dist/core/billing/topup.js +36 -2
- package/dist/core/billing/wireCents.d.ts +8 -0
- package/dist/core/billing/wireCents.js +8 -0
- package/dist/core/billing/workspaceInvites.d.ts +12 -0
- package/dist/core/billing/workspaceInvites.js +26 -0
- package/dist/core/identity.d.ts +6 -6
- package/dist/core/identity.js +4 -10
- package/dist/core/lazyIdentity.d.ts +31 -0
- package/dist/core/lazyIdentity.js +96 -0
- package/dist/core/requestAuth.d.ts +30 -0
- package/dist/core/requestAuth.js +41 -0
- package/dist/core/requestTimeout.d.ts +7 -0
- package/dist/core/requestTimeout.js +13 -0
- package/dist/core/session.d.ts +12 -18
- package/dist/core/session.js +16 -26
- package/dist/core/sessionContracts.d.ts +39 -2
- package/dist/core/sessionTokenMint.d.ts +35 -0
- package/dist/core/sessionTokenMint.js +222 -0
- package/dist/core/webSession.d.ts +27 -0
- package/dist/core/webSession.js +144 -0
- package/dist/core/webSessionFlag.d.ts +19 -0
- package/dist/core/webSessionFlag.js +61 -0
- package/dist/core/webSessionIdentity.d.ts +297 -0
- package/dist/core/webSessionIdentity.js +509 -0
- package/dist/firebase/configSource.d.ts +54 -0
- package/dist/firebase/configSource.js +111 -0
- package/dist/firebase/index.d.ts +71 -8
- package/dist/firebase/index.js +236 -23
- package/dist/firebase/popupWatch.d.ts +42 -0
- package/dist/firebase/popupWatch.js +140 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +116 -0
- package/dist/web/crossTabRefresh.d.ts +4 -2
- package/dist/web/crossTabRefresh.js +13 -0
- package/dist/workspaceLink.d.ts +45 -0
- package/dist/workspaceLink.js +78 -0
- package/package.json +30 -6
|
@@ -9,7 +9,9 @@ export const OPERATION_POLL_TIMING = {
|
|
|
9
9
|
maxMs: 8_000,
|
|
10
10
|
multiplier: 1.5,
|
|
11
11
|
/** An operation parked on the customer is checked on a slow, flat cadence. */
|
|
12
|
-
parkedMs: 30_000
|
|
12
|
+
parkedMs: 30_000,
|
|
13
|
+
/** Fast backoff for an actionless blocked wait: 20 turns of the 3 s PI status cache. */
|
|
14
|
+
actionDiscoveryMs: 60_000
|
|
13
15
|
};
|
|
14
16
|
export const OPERATION_POLL_BUDGET = {
|
|
15
17
|
defaultMs: 120_000,
|
|
@@ -18,6 +20,40 @@ export const OPERATION_POLL_BUDGET = {
|
|
|
18
20
|
/** Once the customer is involved, a bank challenge can idle for hours. */
|
|
19
21
|
customerActionMs: 23 * 60 * 60_000
|
|
20
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* The one "customer can act here" rule both billing rails park on: a
|
|
25
|
+
* retryable failure to retry, a hosted page to visit, or an embedded
|
|
26
|
+
* challenge the server still says is required. A non-retryable decline never
|
|
27
|
+
* reaches it; the operation is terminal by then.
|
|
28
|
+
*/
|
|
29
|
+
export function customerCanActHere(hold) {
|
|
30
|
+
if (hold.authenticationState === 'failed_retryable')
|
|
31
|
+
return true;
|
|
32
|
+
if (hold.offersHostedPage)
|
|
33
|
+
return true;
|
|
34
|
+
return (hold.authenticationState === 'requires_action' &&
|
|
35
|
+
(hold.challenge === 'required' || hold.challenge === 'in_progress'));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The SDK renders one presentation: a hosted operation offers only its page,
|
|
39
|
+
* an embedded one only its challenge. The cloud app's
|
|
40
|
+
* `legacyOperationActionHold` names the rows where the two surfaces differ.
|
|
41
|
+
*/
|
|
42
|
+
export function pendingOperationActionHold(state) {
|
|
43
|
+
return state.presentation === 'hosted'
|
|
44
|
+
? {
|
|
45
|
+
authenticationState: state.authenticationState,
|
|
46
|
+
offersHostedPage: state.actionUrl !== undefined
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
authenticationState: state.authenticationState,
|
|
50
|
+
offersHostedPage: false,
|
|
51
|
+
challenge: state.challenge?.status
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function customerCanAct(state) {
|
|
55
|
+
return customerCanActHere(pendingOperationActionHold(state));
|
|
56
|
+
}
|
|
21
57
|
/**
|
|
22
58
|
* Waiting on the customer, not on the backend: a challenge to complete
|
|
23
59
|
* elsewhere, a hosted page to finish, a phase the server reports as blocked on
|
|
@@ -25,16 +61,41 @@ export const OPERATION_POLL_BUDGET = {
|
|
|
25
61
|
* challenge completes the state reads processing and nothing waits on the
|
|
26
62
|
* customer anymore.
|
|
27
63
|
*/
|
|
28
|
-
|
|
64
|
+
function isWaitingOnCustomer(state) {
|
|
29
65
|
return (state.authenticationState === 'requires_action' ||
|
|
30
66
|
state.actionUrl !== undefined ||
|
|
31
67
|
isBlockedOnCustomerPhase(state.serverPhase) ||
|
|
32
68
|
(state.authenticationState === 'failed_retryable' &&
|
|
33
69
|
state.customerActionSeen));
|
|
34
70
|
}
|
|
35
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Parked only while the customer can act here. The server can report a
|
|
73
|
+
* blocked phase and a client secret before its cached `authentication_state`
|
|
74
|
+
* catches up, and the slow cadence would then hold a screen with no action.
|
|
75
|
+
*/
|
|
76
|
+
export function isParkedOnCustomer(state) {
|
|
77
|
+
return customerCanAct(state) && isWaitingOnCustomer(state);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Blocked on the customer with nothing to offer them yet: either the action
|
|
81
|
+
* is still on its way, or it belongs to someone else (a member without
|
|
82
|
+
* billing permission, a tab without embedded checkout).
|
|
83
|
+
*/
|
|
84
|
+
export function isWaitingOnCustomerWithoutAction(state) {
|
|
85
|
+
return !customerCanAct(state) && isWaitingOnCustomer(state);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `waitedWithoutActionMs` is how long the operation has continuously been
|
|
89
|
+
* {@link isWaitingOnCustomerWithoutAction}; past the discovery window it
|
|
90
|
+
* parks too, so an action that never arrives here costs the slow cadence.
|
|
91
|
+
*/
|
|
92
|
+
export function nextPollDelayMs(state, previousDelayMs, waitedWithoutActionMs = 0) {
|
|
36
93
|
if (isParkedOnCustomer(state))
|
|
37
94
|
return OPERATION_POLL_TIMING.parkedMs;
|
|
95
|
+
if (isWaitingOnCustomerWithoutAction(state) &&
|
|
96
|
+
waitedWithoutActionMs >= OPERATION_POLL_TIMING.actionDiscoveryMs) {
|
|
97
|
+
return OPERATION_POLL_TIMING.parkedMs;
|
|
98
|
+
}
|
|
38
99
|
return Math.min((previousDelayMs ?? OPERATION_POLL_TIMING.initialMs) *
|
|
39
100
|
OPERATION_POLL_TIMING.multiplier, OPERATION_POLL_TIMING.maxMs);
|
|
40
101
|
}
|
|
@@ -1,20 +1,193 @@
|
|
|
1
|
-
|
|
2
|
-
* One billing operation as the SDK sees it: a discriminated union over the
|
|
3
|
-
* lifecycle a `billing_op_id` moves through, and the pure transition that
|
|
4
|
-
* advances it. The identity is fixed at adoption and survives every
|
|
5
|
-
* presentation change, so switching a checkout from the embedded challenge
|
|
6
|
-
* to the hosted page (or back) is a transition on the same operation, never
|
|
7
|
-
* a replacement one.
|
|
8
|
-
*
|
|
9
|
-
* The transition consumes the generated `BillingOpStatusResponse`. It reads
|
|
10
|
-
* the coded `decline_reason` and `recovery_action` and never `error_message`,
|
|
11
|
-
* so no server or payment-provider text can reach a consumer through this
|
|
12
|
-
* state.
|
|
13
|
-
*/
|
|
14
|
-
import type { zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
|
|
15
|
-
import type { z } from 'zod';
|
|
1
|
+
import { z } from 'zod';
|
|
16
2
|
import type { BillingScope } from './billingScope.js';
|
|
17
|
-
export
|
|
3
|
+
export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
4
|
+
action_url: z.ZodOptional<z.ZodString>;
|
|
5
|
+
authentication_state: z.ZodOptional<z.ZodEnum<["requires_action", "processing", "failed_retryable", "succeeded", "reconciliation_needed"]>>;
|
|
6
|
+
completed_at: z.ZodOptional<z.ZodString>;
|
|
7
|
+
decline_reason: z.ZodOptional<z.ZodEnum<["card_declined", "insufficient_funds", "expired_card", "incorrect_cvc", "authentication_required", "authentication_failed", "processing_error", "payment_not_completed", "generic"]>>;
|
|
8
|
+
error_message: z.ZodOptional<z.ZodString>;
|
|
9
|
+
id: z.ZodString;
|
|
10
|
+
payment_intent_client_secret: z.ZodOptional<z.ZodString>;
|
|
11
|
+
phase: z.ZodOptional<z.ZodEnum<["awaiting_payment_method", "awaiting_invoice_payment", "in_progress"]>>;
|
|
12
|
+
plan: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
14
|
+
slug: z.ZodString;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
17
|
+
slug: string;
|
|
18
|
+
}, {
|
|
19
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
20
|
+
slug: string;
|
|
21
|
+
}>>;
|
|
22
|
+
recovery_action: z.ZodOptional<z.ZodEnum<["retry", "replace_payment_method", "authenticate_payment", "contact_support"]>>;
|
|
23
|
+
retryable: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
started_at: z.ZodString;
|
|
25
|
+
status: z.ZodEnum<["pending", "succeeded", "failed", "reconciliation_needed"]>;
|
|
26
|
+
} & {
|
|
27
|
+
amount_charged_cents: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
credits_added: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
charge_breakdown: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
currency: z.ZodString;
|
|
31
|
+
prorated: z.ZodBoolean;
|
|
32
|
+
} & {
|
|
33
|
+
amount_charged_cents: z.ZodNumber;
|
|
34
|
+
reasons: z.ZodArray<z.ZodObject<{
|
|
35
|
+
kind: z.ZodEnum<["promo_code", "subscription_discount", "account_balance"]>;
|
|
36
|
+
} & {
|
|
37
|
+
amount_cents: z.ZodNumber;
|
|
38
|
+
discount: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
code: z.ZodString;
|
|
40
|
+
duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
|
|
41
|
+
kind: z.ZodEnum<["plan", "promotion"]>;
|
|
42
|
+
name: z.ZodOptional<z.ZodString>;
|
|
43
|
+
} & {
|
|
44
|
+
amount_off_cents: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
duration_in_months: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
code: string;
|
|
48
|
+
kind: "plan" | "promotion";
|
|
49
|
+
name?: string | undefined;
|
|
50
|
+
amount_off_cents?: number | undefined;
|
|
51
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
52
|
+
duration_in_months?: number | undefined;
|
|
53
|
+
}, {
|
|
54
|
+
code: string;
|
|
55
|
+
kind: "plan" | "promotion";
|
|
56
|
+
name?: string | undefined;
|
|
57
|
+
amount_off_cents?: number | undefined;
|
|
58
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
59
|
+
duration_in_months?: number | undefined;
|
|
60
|
+
}>>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
amount_cents: number;
|
|
63
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
64
|
+
discount?: {
|
|
65
|
+
code: string;
|
|
66
|
+
kind: "plan" | "promotion";
|
|
67
|
+
name?: string | undefined;
|
|
68
|
+
amount_off_cents?: number | undefined;
|
|
69
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
70
|
+
duration_in_months?: number | undefined;
|
|
71
|
+
} | undefined;
|
|
72
|
+
}, {
|
|
73
|
+
amount_cents: number;
|
|
74
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
75
|
+
discount?: {
|
|
76
|
+
code: string;
|
|
77
|
+
kind: "plan" | "promotion";
|
|
78
|
+
name?: string | undefined;
|
|
79
|
+
amount_off_cents?: number | undefined;
|
|
80
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
81
|
+
duration_in_months?: number | undefined;
|
|
82
|
+
} | undefined;
|
|
83
|
+
}>, "many">;
|
|
84
|
+
}, "strip", z.ZodTypeAny, {
|
|
85
|
+
currency: string;
|
|
86
|
+
amount_charged_cents: number;
|
|
87
|
+
prorated: boolean;
|
|
88
|
+
reasons: {
|
|
89
|
+
amount_cents: number;
|
|
90
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
91
|
+
discount?: {
|
|
92
|
+
code: string;
|
|
93
|
+
kind: "plan" | "promotion";
|
|
94
|
+
name?: string | undefined;
|
|
95
|
+
amount_off_cents?: number | undefined;
|
|
96
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
97
|
+
duration_in_months?: number | undefined;
|
|
98
|
+
} | undefined;
|
|
99
|
+
}[];
|
|
100
|
+
}, {
|
|
101
|
+
currency: string;
|
|
102
|
+
amount_charged_cents: number;
|
|
103
|
+
prorated: boolean;
|
|
104
|
+
reasons: {
|
|
105
|
+
amount_cents: number;
|
|
106
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
107
|
+
discount?: {
|
|
108
|
+
code: string;
|
|
109
|
+
kind: "plan" | "promotion";
|
|
110
|
+
name?: string | undefined;
|
|
111
|
+
amount_off_cents?: number | undefined;
|
|
112
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
113
|
+
duration_in_months?: number | undefined;
|
|
114
|
+
} | undefined;
|
|
115
|
+
}[];
|
|
116
|
+
}>>;
|
|
117
|
+
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
status: "failed" | "pending" | "succeeded" | "reconciliation_needed";
|
|
119
|
+
id: string;
|
|
120
|
+
started_at: string;
|
|
121
|
+
completed_at?: string | undefined;
|
|
122
|
+
error_message?: string | undefined;
|
|
123
|
+
plan?: {
|
|
124
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
125
|
+
slug: string;
|
|
126
|
+
} | undefined;
|
|
127
|
+
action_url?: string | undefined;
|
|
128
|
+
payment_intent_client_secret?: string | undefined;
|
|
129
|
+
amount_charged_cents?: number | undefined;
|
|
130
|
+
authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
|
|
131
|
+
charge_breakdown?: {
|
|
132
|
+
currency: string;
|
|
133
|
+
amount_charged_cents: number;
|
|
134
|
+
prorated: boolean;
|
|
135
|
+
reasons: {
|
|
136
|
+
amount_cents: number;
|
|
137
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
138
|
+
discount?: {
|
|
139
|
+
code: string;
|
|
140
|
+
kind: "plan" | "promotion";
|
|
141
|
+
name?: string | undefined;
|
|
142
|
+
amount_off_cents?: number | undefined;
|
|
143
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
144
|
+
duration_in_months?: number | undefined;
|
|
145
|
+
} | undefined;
|
|
146
|
+
}[];
|
|
147
|
+
} | undefined;
|
|
148
|
+
credits_added?: number | undefined;
|
|
149
|
+
decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
|
|
150
|
+
phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
|
|
151
|
+
recovery_action?: "retry" | "replace_payment_method" | "authenticate_payment" | "contact_support" | undefined;
|
|
152
|
+
retryable?: boolean | undefined;
|
|
153
|
+
}, {
|
|
154
|
+
status: "failed" | "pending" | "succeeded" | "reconciliation_needed";
|
|
155
|
+
id: string;
|
|
156
|
+
started_at: string;
|
|
157
|
+
completed_at?: string | undefined;
|
|
158
|
+
error_message?: string | undefined;
|
|
159
|
+
plan?: {
|
|
160
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
161
|
+
slug: string;
|
|
162
|
+
} | undefined;
|
|
163
|
+
action_url?: string | undefined;
|
|
164
|
+
payment_intent_client_secret?: string | undefined;
|
|
165
|
+
amount_charged_cents?: number | undefined;
|
|
166
|
+
authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
|
|
167
|
+
charge_breakdown?: {
|
|
168
|
+
currency: string;
|
|
169
|
+
amount_charged_cents: number;
|
|
170
|
+
prorated: boolean;
|
|
171
|
+
reasons: {
|
|
172
|
+
amount_cents: number;
|
|
173
|
+
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
174
|
+
discount?: {
|
|
175
|
+
code: string;
|
|
176
|
+
kind: "plan" | "promotion";
|
|
177
|
+
name?: string | undefined;
|
|
178
|
+
amount_off_cents?: number | undefined;
|
|
179
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
180
|
+
duration_in_months?: number | undefined;
|
|
181
|
+
} | undefined;
|
|
182
|
+
}[];
|
|
183
|
+
} | undefined;
|
|
184
|
+
credits_added?: number | undefined;
|
|
185
|
+
decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
|
|
186
|
+
phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
|
|
187
|
+
recovery_action?: "retry" | "replace_payment_method" | "authenticate_payment" | "contact_support" | undefined;
|
|
188
|
+
retryable?: boolean | undefined;
|
|
189
|
+
}>;
|
|
190
|
+
export type BillingOpStatus = z.infer<typeof BillingOpStatusSchema>;
|
|
18
191
|
export type BillingOperationKind = 'subscription' | 'topup' | 'cancel';
|
|
19
192
|
/**
|
|
20
193
|
* Where the customer completes the operation: the challenge this tab drives
|
|
@@ -52,6 +225,13 @@ export type BillingOperationIdentity = BillingPresentationState & {
|
|
|
52
225
|
readonly observedAt: number;
|
|
53
226
|
/** When the attempt began, before the command was issued; telemetry durations count from here. */
|
|
54
227
|
readonly attemptStartedAt: number;
|
|
228
|
+
/**
|
|
229
|
+
* This tab issued the operation and was still waiting on its outcome when
|
|
230
|
+
* it adopted it: from its own command, or after a reload or a return from
|
|
231
|
+
* a provider page. Absent for an operation another tab issued, and for one
|
|
232
|
+
* this tab already saw succeed.
|
|
233
|
+
*/
|
|
234
|
+
readonly awaitedHere?: true;
|
|
55
235
|
};
|
|
56
236
|
/**
|
|
57
237
|
* The in-page challenge for an embedded presentation. `completed` and
|
|
@@ -82,9 +262,22 @@ export type FailedBillingOperation = BillingOperationIdentity & {
|
|
|
82
262
|
readonly recoveryAction?: BillingRecoveryAction;
|
|
83
263
|
readonly retryable: boolean;
|
|
84
264
|
};
|
|
85
|
-
|
|
265
|
+
/**
|
|
266
|
+
* What the server reports a succeeded operation did, for display only. Each
|
|
267
|
+
* fact is absent while the server cannot say it: a charge billed outside a
|
|
268
|
+
* Stripe invoice, a grant not recorded yet, or an operation that is not a
|
|
269
|
+
* plan change.
|
|
270
|
+
*/
|
|
271
|
+
export interface BillingOperationReceipt {
|
|
272
|
+
readonly amountChargedCents?: number;
|
|
273
|
+
readonly creditsAdded?: number;
|
|
274
|
+
readonly plan?: NonNullable<BillingOpStatus['plan']>;
|
|
275
|
+
}
|
|
276
|
+
export type SucceededBillingOperation = BillingOperationIdentity & {
|
|
86
277
|
readonly phase: 'succeeded';
|
|
87
|
-
|
|
278
|
+
readonly receipt?: BillingOperationReceipt;
|
|
279
|
+
};
|
|
280
|
+
export type BillingOperationState = PendingBillingOperation | SucceededBillingOperation | FailedBillingOperation
|
|
88
281
|
/** This tab's poll budget ran out; the server may still settle the operation. */
|
|
89
282
|
| (BillingOperationIdentity & {
|
|
90
283
|
readonly phase: 'timed_out';
|
|
@@ -93,7 +286,10 @@ export type BillingOperationState = PendingBillingOperation | (BillingOperationI
|
|
|
93
286
|
| (BillingOperationIdentity & {
|
|
94
287
|
readonly phase: 'reconciliation_needed';
|
|
95
288
|
})
|
|
96
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* The session or workspace changed underneath it, or the server replaced it
|
|
291
|
+
* with a newer operation; nothing here may be attributed to this tab now.
|
|
292
|
+
*/
|
|
97
293
|
| (BillingOperationIdentity & {
|
|
98
294
|
readonly phase: 'superseded';
|
|
99
295
|
});
|
|
@@ -118,6 +314,11 @@ export type BillingOperationEvent = {
|
|
|
118
314
|
readonly presentation: 'embedded';
|
|
119
315
|
} | {
|
|
120
316
|
readonly type: 'challenge_started';
|
|
317
|
+
}
|
|
318
|
+
/** The server answered a resubmit with a fresh hosted step for this same operation. */
|
|
319
|
+
| {
|
|
320
|
+
readonly type: 'action_reissued';
|
|
321
|
+
readonly actionUrl: string;
|
|
121
322
|
} | {
|
|
122
323
|
readonly type: 'challenge_settled';
|
|
123
324
|
readonly outcome: 'completed' | 'failed';
|
|
@@ -125,4 +326,9 @@ export type BillingOperationEvent = {
|
|
|
125
326
|
export declare function isTerminal(state: BillingOperationState): state is Exclude<BillingOperationState, PendingBillingOperation>;
|
|
126
327
|
/** A continuation link the SDK will hand to a host: absolute and https, nothing else. */
|
|
127
328
|
export declare function validateActionUrl(value: string | undefined): string | undefined;
|
|
329
|
+
/**
|
|
330
|
+
* The charge went through and the server has not recorded its credits yet;
|
|
331
|
+
* reading the operation again fills them in.
|
|
332
|
+
*/
|
|
333
|
+
export declare function isGrantLanding(state: BillingOperationState): boolean;
|
|
128
334
|
export declare function reduceBillingOperation(state: BillingOperationState, event: BillingOperationEvent): BillingOperationState;
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One billing operation as the SDK sees it: a discriminated union over the
|
|
3
|
+
* lifecycle a `billing_op_id` moves through, and the pure transition that
|
|
4
|
+
* advances it. The identity is fixed at adoption and survives every
|
|
5
|
+
* presentation change, so switching a checkout from the embedded challenge
|
|
6
|
+
* to the hosted page (or back) is a transition on the same operation, never
|
|
7
|
+
* a replacement one.
|
|
8
|
+
*
|
|
9
|
+
* The transition consumes the generated `BillingOpStatusResponse`. It reads
|
|
10
|
+
* the coded `decline_reason` and `recovery_action` and never `error_message`,
|
|
11
|
+
* so no server or payment-provider text can reach a consumer through this
|
|
12
|
+
* state.
|
|
13
|
+
*/
|
|
14
|
+
import { zBillingOpChargeBreakdown, zBillingOpChargeReason, zBillingOpStatusResponse, zSubscriptionDiscount } from '@comfyorg/ingest-types/zod';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { wireCents } from './wireCents.js';
|
|
17
|
+
export const BillingOpStatusSchema = zBillingOpStatusResponse.extend({
|
|
18
|
+
amount_charged_cents: wireCents.optional(),
|
|
19
|
+
credits_added: wireCents.optional(),
|
|
20
|
+
charge_breakdown: zBillingOpChargeBreakdown
|
|
21
|
+
.extend({
|
|
22
|
+
amount_charged_cents: wireCents,
|
|
23
|
+
reasons: z.array(zBillingOpChargeReason.extend({
|
|
24
|
+
amount_cents: wireCents,
|
|
25
|
+
discount: zSubscriptionDiscount
|
|
26
|
+
.extend({
|
|
27
|
+
amount_off_cents: wireCents.optional(),
|
|
28
|
+
duration_in_months: wireCents.optional()
|
|
29
|
+
})
|
|
30
|
+
.optional()
|
|
31
|
+
}))
|
|
32
|
+
})
|
|
33
|
+
.optional()
|
|
34
|
+
});
|
|
1
35
|
/**
|
|
2
36
|
* The phases the contract defines as blocked on the customer. Neither advances
|
|
3
37
|
* on its own, so an operation reporting one waits on them even before it has a
|
|
@@ -32,6 +66,7 @@ function identityOf(state) {
|
|
|
32
66
|
scope: state.scope,
|
|
33
67
|
observedAt: state.observedAt,
|
|
34
68
|
attemptStartedAt: state.attemptStartedAt,
|
|
69
|
+
...(state.awaitedHere ? { awaitedHere: true } : {}),
|
|
35
70
|
...presentationOf(state)
|
|
36
71
|
};
|
|
37
72
|
}
|
|
@@ -69,7 +104,7 @@ function nextChallenge(state, status) {
|
|
|
69
104
|
}
|
|
70
105
|
function terminalFromStatus(state, status) {
|
|
71
106
|
if (status.status === 'succeeded')
|
|
72
|
-
return
|
|
107
|
+
return succeeded(state, status);
|
|
73
108
|
if (status.status === 'failed') {
|
|
74
109
|
return {
|
|
75
110
|
...identityOf(state),
|
|
@@ -87,16 +122,62 @@ function terminalFromStatus(state, status) {
|
|
|
87
122
|
}
|
|
88
123
|
return undefined;
|
|
89
124
|
}
|
|
125
|
+
function receiptOf(status) {
|
|
126
|
+
const receipt = {
|
|
127
|
+
...(status.amount_charged_cents === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { amountChargedCents: status.amount_charged_cents }),
|
|
130
|
+
...(status.credits_added === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { creditsAdded: status.credits_added }),
|
|
133
|
+
...(status.plan === undefined ? {} : { plan: status.plan })
|
|
134
|
+
};
|
|
135
|
+
return Object.keys(receipt).length === 0 ? undefined : receipt;
|
|
136
|
+
}
|
|
137
|
+
function succeeded(state, status) {
|
|
138
|
+
const receipt = receiptOf(status);
|
|
139
|
+
return {
|
|
140
|
+
...withPhase(state, 'succeeded'),
|
|
141
|
+
...(receipt === undefined ? {} : { receipt })
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The charge went through and the server has not recorded its credits yet;
|
|
146
|
+
* reading the operation again fills them in.
|
|
147
|
+
*/
|
|
148
|
+
export function isGrantLanding(state) {
|
|
149
|
+
return (state.phase === 'succeeded' &&
|
|
150
|
+
state.receipt?.amountChargedCents !== undefined &&
|
|
151
|
+
state.receipt.creditsAdded === undefined);
|
|
152
|
+
}
|
|
90
153
|
/**
|
|
91
154
|
* A link echoed while this tab's completed challenge is still processing
|
|
92
155
|
* points at that same challenge; surfacing it would ask the customer to
|
|
93
|
-
* redo a step they just finished.
|
|
156
|
+
* redo a step they just finished. A checkout waiting on a card keeps the
|
|
157
|
+
* link a resubmit reissued, because the server stores none for that phase.
|
|
94
158
|
*/
|
|
95
159
|
function nextActionUrl(state, status, authenticationState) {
|
|
96
|
-
|
|
97
|
-
authenticationState !== 'requires_action'
|
|
160
|
+
if (state.challenge?.status === 'completed' &&
|
|
161
|
+
authenticationState !== 'requires_action') {
|
|
162
|
+
return state.actionUrl;
|
|
163
|
+
}
|
|
164
|
+
const served = validateActionUrl(status.action_url);
|
|
165
|
+
return served === undefined && status.phase === 'awaiting_payment_method'
|
|
98
166
|
? state.actionUrl
|
|
99
|
-
:
|
|
167
|
+
: served;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A retryable failure served without a reason reads as `generic`, as a
|
|
171
|
+
* terminal failure does, so the customer is offered the retry the state
|
|
172
|
+
* promises. A challenge this tab saw fail supplies its own reason.
|
|
173
|
+
*/
|
|
174
|
+
function nextDeclineReason(state, status, authenticationState) {
|
|
175
|
+
if (authenticationState !== 'failed_retryable')
|
|
176
|
+
return undefined;
|
|
177
|
+
const known = status.decline_reason ?? state.declineReason;
|
|
178
|
+
if (known !== undefined || state.challenge?.status === 'failed')
|
|
179
|
+
return known;
|
|
180
|
+
return 'generic';
|
|
100
181
|
}
|
|
101
182
|
function reducePending(state, status) {
|
|
102
183
|
if (status.id !== state.id)
|
|
@@ -108,9 +189,7 @@ function reducePending(state, status) {
|
|
|
108
189
|
? state.authenticationState
|
|
109
190
|
: status.authentication_state;
|
|
110
191
|
const actionUrl = nextActionUrl(state, status, authenticationState);
|
|
111
|
-
const declineReason =
|
|
112
|
-
? (status.decline_reason ?? state.declineReason)
|
|
113
|
-
: undefined;
|
|
192
|
+
const declineReason = nextDeclineReason(state, status, authenticationState);
|
|
114
193
|
return {
|
|
115
194
|
...state,
|
|
116
195
|
challenge: nextChallenge(state, status),
|
|
@@ -183,5 +262,11 @@ export function reduceBillingOperation(state, event) {
|
|
|
183
262
|
: state;
|
|
184
263
|
case 'challenge_settled':
|
|
185
264
|
return settleChallenge(state, event.outcome);
|
|
265
|
+
case 'action_reissued': {
|
|
266
|
+
const actionUrl = validateActionUrl(event.actionUrl);
|
|
267
|
+
return actionUrl === undefined
|
|
268
|
+
? state
|
|
269
|
+
: { ...state, actionUrl, customerActionSeen: true };
|
|
270
|
+
}
|
|
186
271
|
}
|
|
187
272
|
}
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
* a host may override — except the safety line, which only the projection's
|
|
4
4
|
* `noChargeConfirmed` may ever unlock.
|
|
5
5
|
*/
|
|
6
|
+
import type { BillingDeclineReason, BillingRecoveryAction } from './operationState.js';
|
|
6
7
|
import type { PaymentProjection, PaymentReasonKey, PaymentStep } from './paymentProjection.js';
|
|
7
|
-
export type PaymentCopyKey = `billing.step.${PaymentStep}.header` | `billing.step.${PaymentStep}.body` | `billing.reason.${PaymentReasonKey}` | 'billing.action.retry' | 'billing.action.continue_verification' | 'billing.safety.nothing_was_charged';
|
|
8
|
+
export type PaymentCopyKey = `billing.step.${PaymentStep}.header` | `billing.step.${PaymentStep}.body` | `billing.reason.${PaymentReasonKey}` | `billing.recovery.${BillingRecoveryAction}` | 'billing.action.retry' | 'billing.action.replace_payment_method' | 'billing.action.contact_support' | 'billing.action.continue_verification' | 'billing.safety.nothing_was_charged';
|
|
8
9
|
declare const SAFETY_KEY = "billing.safety.nothing_was_charged";
|
|
9
10
|
export declare const DEFAULT_PAYMENT_COPY: Readonly<Record<PaymentCopyKey, string>>;
|
|
11
|
+
/** The host's toast detail for a payment the server declined. */
|
|
12
|
+
export type DeclineDetailKey = 'insufficientFundsDetail' | 'expiredCardDetail' | 'incorrectCvcDetail' | 'authenticationFailedDetail' | 'processingErrorDetail' | 'paymentDeclinedDetail';
|
|
13
|
+
export declare function declineDetailKey(reason: BillingDeclineReason): DeclineDetailKey;
|
|
10
14
|
export declare function createPaymentCopy(overrides?: Partial<Record<PaymentCopyKey, string>>): Readonly<Record<PaymentCopyKey, string>>;
|
|
11
15
|
export interface PaymentCopyKeys {
|
|
12
16
|
readonly header: PaymentCopyKey;
|
|
17
|
+
/** The server's recovery line when it names one, else the step's own body. */
|
|
13
18
|
readonly body: PaymentCopyKey;
|
|
14
19
|
readonly reason?: PaymentCopyKey;
|
|
15
20
|
readonly safety?: typeof SAFETY_KEY;
|
|
@@ -24,19 +24,52 @@ export const DEFAULT_PAYMENT_COPY = {
|
|
|
24
24
|
'billing.reason.incorrect_cvc': 'The security code did not match.',
|
|
25
25
|
'billing.reason.authentication_required': 'Your bank requires authentication.',
|
|
26
26
|
'billing.reason.authentication_failed': 'Authentication with your bank did not complete.',
|
|
27
|
+
'billing.reason.payment_not_completed': 'The payment was not completed.',
|
|
27
28
|
'billing.reason.processing_error': 'The payment could not be processed right now.',
|
|
29
|
+
'billing.recovery.retry': 'Please try again.',
|
|
30
|
+
'billing.recovery.replace_payment_method': 'Try again with a different payment method.',
|
|
31
|
+
'billing.recovery.authenticate_payment': 'Try again and complete the verification your bank asks for.',
|
|
32
|
+
'billing.recovery.contact_support': 'This payment cannot be retried. Contact support@comfy.org and we will help you finish it.',
|
|
28
33
|
'billing.action.retry': 'Try again',
|
|
34
|
+
'billing.action.replace_payment_method': 'Use a different payment method',
|
|
35
|
+
'billing.action.contact_support': 'Contact support',
|
|
29
36
|
'billing.action.continue_verification': 'Continue verification',
|
|
30
37
|
[SAFETY_KEY]: 'Nothing was charged.'
|
|
31
38
|
};
|
|
39
|
+
export function declineDetailKey(reason) {
|
|
40
|
+
switch (reason) {
|
|
41
|
+
case 'insufficient_funds':
|
|
42
|
+
return 'insufficientFundsDetail';
|
|
43
|
+
case 'expired_card':
|
|
44
|
+
return 'expiredCardDetail';
|
|
45
|
+
case 'incorrect_cvc':
|
|
46
|
+
return 'incorrectCvcDetail';
|
|
47
|
+
case 'authentication_required':
|
|
48
|
+
case 'authentication_failed':
|
|
49
|
+
case 'payment_not_completed':
|
|
50
|
+
return 'authenticationFailedDetail';
|
|
51
|
+
case 'processing_error':
|
|
52
|
+
return 'processingErrorDetail';
|
|
53
|
+
case 'card_declined':
|
|
54
|
+
case 'generic':
|
|
55
|
+
return 'paymentDeclinedDetail';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
32
58
|
export function createPaymentCopy(overrides = {}) {
|
|
33
59
|
const { [SAFETY_KEY]: _protected, ...safe } = overrides;
|
|
34
60
|
return { ...DEFAULT_PAYMENT_COPY, ...safe };
|
|
35
61
|
}
|
|
62
|
+
function recoveryCopyKey(action) {
|
|
63
|
+
if (action === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
const key = `billing.recovery.${action}`;
|
|
66
|
+
return key in DEFAULT_PAYMENT_COPY ? key : undefined;
|
|
67
|
+
}
|
|
36
68
|
export function paymentCopyKeys(projection) {
|
|
37
69
|
return {
|
|
38
70
|
header: `billing.step.${projection.step}.header`,
|
|
39
|
-
body:
|
|
71
|
+
body: recoveryCopyKey(projection.recoveryAction) ??
|
|
72
|
+
`billing.step.${projection.step}.body`,
|
|
40
73
|
...(projection.reasonKey === undefined
|
|
41
74
|
? {}
|
|
42
75
|
: { reason: `billing.reason.${projection.reasonKey}` }),
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* operation exists; it never advances the machine itself.
|
|
5
5
|
*
|
|
6
6
|
* A server verdict outranks anything the host reports: a customer who backed
|
|
7
|
-
* out of a page after the charge went through still sees the success.
|
|
7
|
+
* out of a page after the charge went through still sees the success. A
|
|
8
|
+
* challenge this tab completed is not offered again while the server settles.
|
|
8
9
|
*/
|
|
9
|
-
import type { BillingDeclineReason, BillingOperationState, BillingRecoveryAction } from './operationState.js';
|
|
10
|
+
import type { BillingDeclineReason, BillingOperationState, BillingRecoveryAction, PendingBillingOperation } from './operationState.js';
|
|
10
11
|
export type PaymentStep = 'select' | 'preview' | 'verifying' | 'canceled' | 'declined' | 'processing_error'
|
|
11
12
|
/** Reserved: the contract cannot yet tell "paid, entitlement pending" from "needs a human". */
|
|
12
13
|
| 'payment_received_hold' | 'success';
|
|
@@ -22,6 +23,11 @@ export type PaymentReasonKey = BillingDeclineReason | 'checkout_expired' | 'gene
|
|
|
22
23
|
export interface PaymentProjection {
|
|
23
24
|
readonly step: PaymentStep;
|
|
24
25
|
readonly reasonKey?: PaymentReasonKey;
|
|
26
|
+
/**
|
|
27
|
+
* What the server tells the customer to do next. A failed operation the
|
|
28
|
+
* server marks non-retryable without naming a recovery reads as
|
|
29
|
+
* `contact_support`, so no dead end ever offers a retry.
|
|
30
|
+
*/
|
|
25
31
|
readonly recoveryAction?: BillingRecoveryAction;
|
|
26
32
|
/** Present whenever an operation backs the projection; the id support can act on. */
|
|
27
33
|
readonly operationId?: string;
|
|
@@ -31,4 +37,10 @@ export interface PaymentProjection {
|
|
|
31
37
|
*/
|
|
32
38
|
readonly noChargeConfirmed: boolean;
|
|
33
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a pending payment's progress reads as a prompt rather than a
|
|
42
|
+
* spinner: only a hosted page the customer still has to visit asks them for
|
|
43
|
+
* anything. An in-page challenge is driven by the host itself.
|
|
44
|
+
*/
|
|
45
|
+
export declare function awaitsHostedAction(state: PendingBillingOperation): boolean;
|
|
34
46
|
export declare function projectPaymentStep(operation: BillingOperationState | undefined, hostStep: HostPaymentStep): PaymentProjection;
|