@comfyorg/account-core 1.0.0-alpha.2 → 1.0.0-alpha.3
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/index.d.ts +4 -3
- package/dist/core/billing/index.js +2 -1
- package/dist/core/billing/operationLifecycle.d.ts +26 -4
- package/dist/core/billing/operationLifecycle.js +113 -14
- package/dist/core/billing/operationPointer.d.ts +24 -0
- package/dist/core/billing/operationPointer.js +14 -1
- package/dist/core/billing/operationState.d.ts +18 -6
- package/dist/core/billing/operationState.js +13 -15
- package/dist/core/billing/paymentFriction.d.ts +17 -0
- package/dist/core/billing/paymentFriction.js +41 -0
- package/dist/core/billing/subscriptionCommands.d.ts +38 -2
- package/dist/core/billing/subscriptionCommands.js +27 -7
- package/dist/core/billing/subscriptionDiscount.d.ts +26 -0
- package/dist/core/billing/subscriptionDiscount.js +6 -0
- package/dist/core/billing/telemetry/billingTelemetryEvent.d.ts +25 -0
- package/dist/core/billing/telemetry/billingTelemetryEvent.js +3 -0
- package/dist/core/billing/telemetry/cancelEvent.d.ts +19 -0
- package/dist/core/billing/telemetry/cancelEvent.js +1 -0
- package/dist/core/billing/telemetry/capabilityReadEvent.d.ts +4 -0
- package/dist/core/billing/telemetry/capabilityReadEvent.js +1 -0
- package/dist/core/billing/telemetry/checkoutChallengeEvent.d.ts +25 -0
- package/dist/core/billing/telemetry/checkoutChallengeEvent.js +1 -0
- package/dist/core/billing/telemetry/checkoutJourney.d.ts +176 -0
- package/dist/core/billing/telemetry/checkoutJourney.js +115 -0
- package/dist/core/billing/telemetry/checkoutRedirectEvent.d.ts +30 -0
- package/dist/core/billing/telemetry/checkoutRedirectEvent.js +1 -0
- package/dist/core/billing/telemetry/downgradeToPersonalEvent.d.ts +14 -0
- package/dist/core/billing/telemetry/downgradeToPersonalEvent.js +1 -0
- package/dist/core/billing/telemetry/entryEvent.d.ts +16 -0
- package/dist/core/billing/telemetry/entryEvent.js +1 -0
- package/dist/core/billing/telemetry/eventNames.d.ts +36 -0
- package/dist/core/billing/telemetry/eventNames.js +36 -0
- package/dist/core/billing/telemetry/index.d.ts +17 -0
- package/dist/core/billing/telemetry/index.js +6 -0
- package/dist/core/billing/telemetry/operationEvent.d.ts +25 -0
- package/dist/core/billing/telemetry/operationEvent.js +1 -0
- package/dist/core/billing/telemetry/operationLifecycleEvent.d.ts +9 -0
- package/dist/core/billing/telemetry/operationLifecycleEvent.js +79 -0
- package/dist/core/billing/telemetry/payload.d.ts +55 -0
- package/dist/core/billing/telemetry/payload.js +75 -0
- package/dist/core/billing/telemetry/portalEvent.d.ts +23 -0
- package/dist/core/billing/telemetry/portalEvent.js +1 -0
- package/dist/core/billing/telemetry/resubscribeEvent.d.ts +13 -0
- package/dist/core/billing/telemetry/resubscribeEvent.js +1 -0
- package/dist/core/billing/telemetry/stages.d.ts +45 -0
- package/dist/core/billing/telemetry/stages.js +1 -0
- package/dist/core/billing/telemetry/subscriptionCheckoutEvent.d.ts +23 -0
- package/dist/core/billing/telemetry/subscriptionCheckoutEvent.js +1 -0
- package/dist/core/billing/telemetry/topupEvent.d.ts +30 -0
- package/dist/core/billing/telemetry/topupEvent.js +6 -0
- package/dist/core/billing/telemetry/vocabulary.d.ts +9 -0
- package/dist/core/billing/telemetry/vocabulary.js +1 -0
- package/dist/core/billing/telemetry/webEntryEvent.d.ts +27 -0
- package/dist/core/billing/telemetry/webEntryEvent.js +1 -0
- package/dist/core/billing/telemetry/webHandoffEvent.d.ts +11 -0
- package/dist/core/billing/telemetry/webHandoffEvent.js +1 -0
- package/dist/core/billing/telemetry/webReturnEvent.d.ts +8 -0
- package/dist/core/billing/telemetry/webReturnEvent.js +1 -0
- package/dist/core/billing/telemetry/webSessionEvent.d.ts +17 -0
- package/dist/core/billing/telemetry/webSessionEvent.js +1 -0
- package/dist/core/webSession.d.ts +4 -3
- package/dist/core/webSession.js +6 -5
- package/dist/core/webSessionIdentity.d.ts +6 -6
- package/dist/signInSchemas.d.ts +1 -1
- package/dist/telemetry.d.ts +26 -0
- package/dist/telemetry.js +28 -7
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +5 -4
- package/package.json +3 -1
|
@@ -31,7 +31,7 @@ export type { PaymentMethodsReadOptions, PaymentMethodsReader, PaymentMethodsRea
|
|
|
31
31
|
export { PAYMENT_METHODS_ROUTE, createPaymentMethodsReader } from './paymentMethods.js';
|
|
32
32
|
export type { BillingStatusData, BillingStatusReader, BillingStatusReaderOptions, BillingStatusReadOptions, BillingStatusScope, BillingStatusSnapshot } from './status.js';
|
|
33
33
|
export { BILLING_STATUS_ROUTE, createBillingStatusReader } from './status.js';
|
|
34
|
-
export type { BillingAuthenticationState, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationReceipt, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation, SucceededBillingOperation } from './operationState.js';
|
|
34
|
+
export type { BillingAuthenticationState, BillingChargeBreakdown, BillingChargeReason, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationReceipt, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation, SucceededBillingOperation } from './operationState.js';
|
|
35
35
|
export { isBlockedOnCustomerPhase, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
36
36
|
export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pendingOperationActionHold, pollBudgetMs } from './operationPolicy.js';
|
|
37
37
|
export type { CustomerActionHold } from './operationPolicy.js';
|
|
@@ -41,11 +41,12 @@ export type { PresentationRoutingInput } from './presentation.js';
|
|
|
41
41
|
export { selectBillingPresentation } from './presentation.js';
|
|
42
42
|
export type { BillingOperationFailureCategory, BillingOperationLifecycle, BillingOperationLifecycleOptions, BillingOperationTelemetryEvent, BillingRecoverOptions, IssuedBillingOperation, PresentationSwitchOutcome } from './operationLifecycle.js';
|
|
43
43
|
export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
|
|
44
|
-
export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
44
|
+
export { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
45
|
+
export * from './telemetry/index.js';
|
|
45
46
|
export type { EmbeddedChallengeOutcome, EmbeddedChallengePort } from './challengeDriver.js';
|
|
46
47
|
export { driveEmbeddedChallenge } from './challengeDriver.js';
|
|
47
48
|
export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
|
|
48
|
-
export type { BillingCommands, BillingCommandsOptions, PaymentPortalResult, PreviewSubscribeInput, PreviewSubscribeOptions, PreviewSubscribeResult, SubscribeInput, SubscriptionCommandCode, SubscriptionCommandFailure, SubscriptionCommandOutcome, SubscriptionCommandResult, SubscriptionPreview, TerminalBillingOperation } from './subscriptionCommands.js';
|
|
49
|
+
export type { BillingCommands, BillingCommandsOptions, CancelOperationResult, CancelRefusalCode, PaymentPortalResult, PreviewSubscribeInput, PreviewSubscribeOptions, PreviewSubscribeResult, SubscribeInput, SubscriptionCommandCode, SubscriptionCommandFailure, SubscriptionCommandOutcome, SubscriptionCommandResult, SubscriptionPreview, TerminalBillingOperation } from './subscriptionCommands.js';
|
|
49
50
|
export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
|
|
50
51
|
export type { HostPaymentStep, PaymentProjection, PaymentReasonKey, PaymentStep } from './paymentProjection.js';
|
|
51
52
|
export { awaitsHostedAction, projectPaymentStep } from './paymentProjection.js';
|
|
@@ -15,7 +15,8 @@ export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasEx
|
|
|
15
15
|
export { OPERATION_POINTER_MAX_AGE_MS, createOperationPointerStore, operationPointerKey } from './operationPointer.js';
|
|
16
16
|
export { selectBillingPresentation } from './presentation.js';
|
|
17
17
|
export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
|
|
18
|
-
export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
18
|
+
export { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
19
|
+
export * from './telemetry/index.js';
|
|
19
20
|
export { driveEmbeddedChallenge } from './challengeDriver.js';
|
|
20
21
|
export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
|
|
21
22
|
export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
* wiring onto the scope source, the scope tracker, and the generated
|
|
17
17
|
* contract is new.
|
|
18
18
|
*/
|
|
19
|
-
import { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
19
|
+
import { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
20
20
|
import type { BillingResult, BillingTransport } from './billingContracts.js';
|
|
21
21
|
import type { BillingScope, BillingScopeSource } from './billingScope.js';
|
|
22
22
|
import type { BillingOperationPointerStorage } from './operationPointer.js';
|
|
23
23
|
import type { BillingDeclineReason, BillingOperationKind, BillingOperationState, BillingPresentation, HostedBillingDestination } from './operationState.js';
|
|
24
|
+
import type { CheckoutHostedStep, CheckoutRedirectNavigation } from './telemetry/checkoutRedirectEvent.js';
|
|
25
|
+
import type { CheckoutMethodKind } from './telemetry/checkoutJourney.js';
|
|
24
26
|
import type { BillingStatusReader } from './status.js';
|
|
25
27
|
export declare function operationRoute(operationId: string): string;
|
|
26
28
|
/** What a command learned from the backend when it issued the operation. */
|
|
@@ -32,18 +34,31 @@ export interface IssuedBillingOperation {
|
|
|
32
34
|
readonly clientSecret?: string;
|
|
33
35
|
}
|
|
34
36
|
export type BillingOperationFailureCategory = 'provider_decline' | 'api_rejected' | 'poll_timeout' | 'reconciliation_needed' | 'stale_operation';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
type HostedStepEventName = typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.redirectStarted | typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned;
|
|
38
|
+
type BillingOperationTelemetryEventName = Exclude<(typeof BILLING_OPERATION_TELEMETRY_EVENT)[keyof typeof BILLING_OPERATION_TELEMETRY_EVENT] | (typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT)[keyof typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT], HostedStepEventName>;
|
|
39
|
+
/** A hosted step this tab handed the customer to. */
|
|
40
|
+
interface HostedStepVisit {
|
|
41
|
+
readonly destination: HostedBillingDestination;
|
|
42
|
+
readonly step: CheckoutHostedStep;
|
|
43
|
+
readonly navigation: CheckoutRedirectNavigation;
|
|
44
|
+
readonly method_kind?: CheckoutMethodKind;
|
|
45
|
+
}
|
|
46
|
+
interface BillingOperationTelemetryBase {
|
|
37
47
|
readonly billing_op_id: string;
|
|
38
48
|
readonly operation_type: BillingOperationKind;
|
|
39
49
|
readonly presentation: BillingPresentation;
|
|
40
50
|
/** True when this tab reattached to an operation it did not issue. */
|
|
41
51
|
readonly resumed: boolean;
|
|
52
|
+
}
|
|
53
|
+
export type BillingOperationTelemetryEvent = BillingOperationTelemetryBase & ({
|
|
54
|
+
readonly name: BillingOperationTelemetryEventName;
|
|
42
55
|
readonly failure_category?: BillingOperationFailureCategory;
|
|
43
56
|
readonly decline_reason?: BillingDeclineReason;
|
|
44
57
|
/** From the attempt's start, so a resumed operation reports its whole life. */
|
|
45
58
|
readonly duration_ms?: number;
|
|
46
|
-
}
|
|
59
|
+
} | (HostedStepVisit & {
|
|
60
|
+
readonly name: HostedStepEventName;
|
|
61
|
+
}));
|
|
47
62
|
export type PresentationSwitchOutcome = 'switched' | 'unchanged'
|
|
48
63
|
/** The server has offered no hosted continuation yet. */
|
|
49
64
|
| 'no_hosted_url'
|
|
@@ -97,6 +112,12 @@ export interface BillingOperationLifecycle {
|
|
|
97
112
|
wake: () => void;
|
|
98
113
|
/** Moves the operation between presentations under the same id. */
|
|
99
114
|
switchPresentation: (operationId: string, presentation: BillingPresentation) => PresentationSwitchOutcome;
|
|
115
|
+
/**
|
|
116
|
+
* The host opened the operation's hosted step. A redirect is remembered in
|
|
117
|
+
* the pointer, so the page it returns to reports the return; a new tab
|
|
118
|
+
* reports it on the next wake.
|
|
119
|
+
*/
|
|
120
|
+
reportHostedStepOpened: (operationId: string, navigation: CheckoutRedirectNavigation, methodKind?: CheckoutMethodKind) => void;
|
|
100
121
|
/** The host began driving the operation's challenge; polling pauses until it settles. */
|
|
101
122
|
reportChallengeStarted: (operationId: string) => void;
|
|
102
123
|
reportChallengeSettled: (operationId: string, outcome: 'completed' | 'failed') => void;
|
|
@@ -108,3 +129,4 @@ export interface BillingOperationLifecycle {
|
|
|
108
129
|
dispose: () => void;
|
|
109
130
|
}
|
|
110
131
|
export declare function createBillingOperationLifecycle(options: BillingOperationLifecycleOptions): BillingOperationLifecycle;
|
|
132
|
+
export {};
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* wiring onto the scope source, the scope tracker, and the generated
|
|
17
17
|
* contract is new.
|
|
18
18
|
*/
|
|
19
|
-
import { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
19
|
+
import { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
20
20
|
import { createBillingScopeTracker } from './billingScope.js';
|
|
21
21
|
import { NO_POINTER_STORE, createOperationPointerStore } from './operationPointer.js';
|
|
22
22
|
import { hasExhaustedPollBudget, isWaitingOnCustomerWithoutAction, nextPollDelayMs } from './operationPolicy.js';
|
|
23
23
|
import { BillingOpStatusSchema, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
24
|
+
import { paymentFrictionBetween } from './paymentFriction.js';
|
|
24
25
|
import { selectBillingPresentation } from './presentation.js';
|
|
25
26
|
import { readValidatedBillingResponse } from './sharedRead.js';
|
|
26
27
|
/** Settled, but a later read may still change what the operation reports. */
|
|
@@ -42,6 +43,18 @@ const OPERATION_ALREADY_PENDING = {
|
|
|
42
43
|
status: 'error',
|
|
43
44
|
code: 'OPERATION_ALREADY_PENDING'
|
|
44
45
|
};
|
|
46
|
+
/** The attempt a pointer remembers, picked up again by this tab. */
|
|
47
|
+
function resumedFrom(pointer) {
|
|
48
|
+
return {
|
|
49
|
+
presentation: pointer.presentation,
|
|
50
|
+
attemptStartedAt: pointer.attemptStartedAt,
|
|
51
|
+
resumed: true,
|
|
52
|
+
awaitedHere: pointer.awaited === true,
|
|
53
|
+
...(pointer.redirect === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { returnedFrom: pointer.redirect })
|
|
56
|
+
};
|
|
57
|
+
}
|
|
45
58
|
function continuationOf(issued) {
|
|
46
59
|
return {
|
|
47
60
|
...(issued.actionUrl === undefined ? {} : { actionUrl: issued.actionUrl }),
|
|
@@ -95,6 +108,23 @@ function failureCategoryFor(state) {
|
|
|
95
108
|
return 'stale_operation';
|
|
96
109
|
}
|
|
97
110
|
}
|
|
111
|
+
const FRICTION_EVENT_NAME = {
|
|
112
|
+
challenge_required: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeRequired,
|
|
113
|
+
challenge_completed: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeCompleted,
|
|
114
|
+
challenge_failed: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeFailed
|
|
115
|
+
};
|
|
116
|
+
/** What the hosted page asks of the customer, from what the server waits on. */
|
|
117
|
+
function hostedStepOf(state) {
|
|
118
|
+
if (state.authenticationState === 'requires_action' ||
|
|
119
|
+
(state.presentation === 'embedded' && state.challenge !== undefined)) {
|
|
120
|
+
return 'authentication';
|
|
121
|
+
}
|
|
122
|
+
if (state.serverPhase === 'awaiting_payment_method')
|
|
123
|
+
return 'payment_method';
|
|
124
|
+
if (state.serverPhase === 'awaiting_invoice_payment')
|
|
125
|
+
return 'invoice_payment';
|
|
126
|
+
return 'checkout';
|
|
127
|
+
}
|
|
98
128
|
/** The pause is this tab's own challenge on screen; a hosted page never pauses. */
|
|
99
129
|
function isDrivingChallenge(state) {
|
|
100
130
|
return (state.presentation === 'embedded' &&
|
|
@@ -155,11 +185,28 @@ export function createBillingOperationLifecycle(options) {
|
|
|
155
185
|
duration_ms: now() - state.attemptStartedAt
|
|
156
186
|
});
|
|
157
187
|
}
|
|
188
|
+
function emitFrictionTelemetry(record, before) {
|
|
189
|
+
const state = record.state;
|
|
190
|
+
for (const signal of paymentFrictionBetween(before, state)) {
|
|
191
|
+
onTelemetry?.({
|
|
192
|
+
name: FRICTION_EVENT_NAME[signal.stage],
|
|
193
|
+
billing_op_id: state.id,
|
|
194
|
+
operation_type: state.kind,
|
|
195
|
+
presentation: state.presentation,
|
|
196
|
+
resumed: record.resumed,
|
|
197
|
+
...('declineReason' in signal && signal.declineReason !== undefined
|
|
198
|
+
? { decline_reason: signal.declineReason }
|
|
199
|
+
: {})
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
158
203
|
function dispatch(record, event) {
|
|
159
|
-
const
|
|
160
|
-
|
|
204
|
+
const before = record.state;
|
|
205
|
+
const next = reduceBillingOperation(before, event);
|
|
206
|
+
if (next === before)
|
|
161
207
|
return;
|
|
162
208
|
record.state = next;
|
|
209
|
+
emitFrictionTelemetry(record, before);
|
|
163
210
|
if (isTerminal(next)) {
|
|
164
211
|
stopTimer(record);
|
|
165
212
|
pointers.settle(next);
|
|
@@ -195,13 +242,28 @@ export function createBillingOperationLifecycle(options) {
|
|
|
195
242
|
record.inFlightPoll = request;
|
|
196
243
|
return request;
|
|
197
244
|
}
|
|
198
|
-
function
|
|
245
|
+
function savedRedirect(scope, operationId) {
|
|
246
|
+
const saved = pointers.read(scope);
|
|
247
|
+
return saved?.operationId === operationId ? saved.redirect : undefined;
|
|
248
|
+
}
|
|
249
|
+
function writePointer(scope, state, redirect) {
|
|
199
250
|
pointers.write(scope, {
|
|
200
251
|
operationId: state.id,
|
|
201
252
|
kind: state.kind,
|
|
202
253
|
presentation: state.presentation,
|
|
203
254
|
attemptStartedAt: state.attemptStartedAt,
|
|
204
|
-
...(state.awaitedHere ? { awaited: true } : {})
|
|
255
|
+
...(state.awaitedHere ? { awaited: true } : {}),
|
|
256
|
+
...(redirect === undefined ? {} : { redirect })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function emitHostedStepTelemetry(record, name, visit) {
|
|
260
|
+
onTelemetry?.({
|
|
261
|
+
name,
|
|
262
|
+
billing_op_id: record.state.id,
|
|
263
|
+
operation_type: record.state.kind,
|
|
264
|
+
presentation: record.state.presentation,
|
|
265
|
+
resumed: record.resumed,
|
|
266
|
+
...visit
|
|
205
267
|
});
|
|
206
268
|
}
|
|
207
269
|
function readOperation(operationId) {
|
|
@@ -292,6 +354,7 @@ export function createBillingOperationLifecycle(options) {
|
|
|
292
354
|
waitingWithoutActionSince: undefined,
|
|
293
355
|
timer: undefined,
|
|
294
356
|
inFlightPoll: undefined,
|
|
357
|
+
awaitingReturn: undefined,
|
|
295
358
|
settled,
|
|
296
359
|
resolveSettled
|
|
297
360
|
};
|
|
@@ -304,6 +367,13 @@ export function createBillingOperationLifecycle(options) {
|
|
|
304
367
|
presentation: input.presentation,
|
|
305
368
|
resumed: input.resumed
|
|
306
369
|
});
|
|
370
|
+
emitFrictionTelemetry(record, undefined);
|
|
371
|
+
if (input.returnedFrom !== undefined) {
|
|
372
|
+
emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned, {
|
|
373
|
+
...input.returnedFrom,
|
|
374
|
+
navigation: 'redirect'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
307
377
|
startObserving(record, input, state);
|
|
308
378
|
return record;
|
|
309
379
|
}
|
|
@@ -450,10 +520,7 @@ export function createBillingOperationLifecycle(options) {
|
|
|
450
520
|
id: pointer.operationId,
|
|
451
521
|
kind: pointer.kind,
|
|
452
522
|
context,
|
|
453
|
-
|
|
454
|
-
attemptStartedAt: pointer.attemptStartedAt,
|
|
455
|
-
resumed: true,
|
|
456
|
-
awaitedHere: pointer.awaited === true
|
|
523
|
+
...resumedFrom(pointer)
|
|
457
524
|
};
|
|
458
525
|
}
|
|
459
526
|
// Unreachable is not "nothing pending": the pointer is the only evidence
|
|
@@ -470,10 +537,14 @@ export function createBillingOperationLifecycle(options) {
|
|
|
470
537
|
const record = adopt({
|
|
471
538
|
...pending,
|
|
472
539
|
context,
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
540
|
+
...(known === undefined
|
|
541
|
+
? {
|
|
542
|
+
presentation: routeFor(rail, pending),
|
|
543
|
+
attemptStartedAt: now(),
|
|
544
|
+
resumed: true,
|
|
545
|
+
awaitedHere: false
|
|
546
|
+
}
|
|
547
|
+
: resumedFrom(known))
|
|
477
548
|
}, includeSettled);
|
|
478
549
|
return { status: 'ok', value: record.state };
|
|
479
550
|
}
|
|
@@ -524,6 +595,7 @@ export function createBillingOperationLifecycle(options) {
|
|
|
524
595
|
}
|
|
525
596
|
function wake() {
|
|
526
597
|
for (const record of operations.values()) {
|
|
598
|
+
reportReturnFromNewTab(record);
|
|
527
599
|
if (record.state.phase !== 'pending')
|
|
528
600
|
continue;
|
|
529
601
|
if (isDrivingChallenge(record.state))
|
|
@@ -561,11 +633,37 @@ export function createBillingOperationLifecycle(options) {
|
|
|
561
633
|
if (refusal !== undefined)
|
|
562
634
|
return refusal;
|
|
563
635
|
dispatch(record, switchEvent(presentation));
|
|
564
|
-
writePointer(record.context.scope, record.state);
|
|
636
|
+
writePointer(record.context.scope, record.state, savedRedirect(record.context.scope, operationId));
|
|
565
637
|
record.delayMs = undefined;
|
|
566
638
|
schedule(record);
|
|
567
639
|
return 'switched';
|
|
568
640
|
}
|
|
641
|
+
function reportReturnFromNewTab(record) {
|
|
642
|
+
const visit = record.awaitingReturn;
|
|
643
|
+
if (visit === undefined)
|
|
644
|
+
return;
|
|
645
|
+
record.awaitingReturn = undefined;
|
|
646
|
+
emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned, visit);
|
|
647
|
+
}
|
|
648
|
+
function reportHostedStepOpened(operationId, navigation, methodKind) {
|
|
649
|
+
const record = operations.get(operationId);
|
|
650
|
+
if (record === undefined || record.state.phase !== 'pending')
|
|
651
|
+
return;
|
|
652
|
+
const state = record.state;
|
|
653
|
+
const redirect = {
|
|
654
|
+
destination: state.presentation === 'hosted' ? state.hostedDestination : 'stripe',
|
|
655
|
+
step: hostedStepOf(state),
|
|
656
|
+
...(methodKind === undefined ? {} : { method_kind: methodKind })
|
|
657
|
+
};
|
|
658
|
+
const visit = { ...redirect, navigation };
|
|
659
|
+
if (navigation === 'redirect') {
|
|
660
|
+
writePointer(record.context.scope, state, redirect);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
record.awaitingReturn = visit;
|
|
664
|
+
}
|
|
665
|
+
emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.redirectStarted, visit);
|
|
666
|
+
}
|
|
569
667
|
function reportChallengeStarted(operationId) {
|
|
570
668
|
const record = operations.get(operationId);
|
|
571
669
|
if (record === undefined)
|
|
@@ -594,6 +692,7 @@ export function createBillingOperationLifecycle(options) {
|
|
|
594
692
|
recover,
|
|
595
693
|
wake,
|
|
596
694
|
switchPresentation,
|
|
695
|
+
reportHostedStepOpened,
|
|
597
696
|
reportChallengeStarted,
|
|
598
697
|
reportChallengeSettled,
|
|
599
698
|
get: (operationId) => operations.get(operationId)?.state,
|
|
@@ -26,6 +26,20 @@ declare const PointerSchema: z.ZodObject<{
|
|
|
26
26
|
awaited: z.ZodOptional<z.ZodLiteral<true>>;
|
|
27
27
|
/** The end this tab saw it reach, kept only by a store that retains settled pointers. */
|
|
28
28
|
settled: z.ZodOptional<z.ZodEnum<["succeeded", "reconciliation_needed"]>>;
|
|
29
|
+
/** The hosted step this page redirected to; the page it returns to reports the return. */
|
|
30
|
+
redirect: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
destination: z.ZodEnum<["stripe", "billing_web"]>;
|
|
32
|
+
step: z.ZodEnum<["authentication", "payment_method", "invoice_payment", "checkout"]>;
|
|
33
|
+
method_kind: z.ZodOptional<z.ZodEnum<["card", "alipay", "other"]>>;
|
|
34
|
+
}, "strip", z.ZodTypeAny, {
|
|
35
|
+
step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
|
|
36
|
+
destination: "stripe" | "billing_web";
|
|
37
|
+
method_kind?: "card" | "alipay" | "other" | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
|
|
40
|
+
destination: "stripe" | "billing_web";
|
|
41
|
+
method_kind?: "card" | "alipay" | "other" | undefined;
|
|
42
|
+
}>>;
|
|
29
43
|
}, "strip", z.ZodTypeAny, {
|
|
30
44
|
kind: "cancel" | "subscription" | "topup";
|
|
31
45
|
attemptStartedAt: number;
|
|
@@ -33,6 +47,11 @@ declare const PointerSchema: z.ZodObject<{
|
|
|
33
47
|
operationId: string;
|
|
34
48
|
awaited?: true | undefined;
|
|
35
49
|
settled?: "succeeded" | "reconciliation_needed" | undefined;
|
|
50
|
+
redirect?: {
|
|
51
|
+
step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
|
|
52
|
+
destination: "stripe" | "billing_web";
|
|
53
|
+
method_kind?: "card" | "alipay" | "other" | undefined;
|
|
54
|
+
} | undefined;
|
|
36
55
|
}, {
|
|
37
56
|
kind: "cancel" | "subscription" | "topup";
|
|
38
57
|
attemptStartedAt: number;
|
|
@@ -40,6 +59,11 @@ declare const PointerSchema: z.ZodObject<{
|
|
|
40
59
|
operationId: string;
|
|
41
60
|
awaited?: true | undefined;
|
|
42
61
|
settled?: "succeeded" | "reconciliation_needed" | undefined;
|
|
62
|
+
redirect?: {
|
|
63
|
+
step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
|
|
64
|
+
destination: "stripe" | "billing_web";
|
|
65
|
+
method_kind?: "card" | "alipay" | "other" | undefined;
|
|
66
|
+
} | undefined;
|
|
43
67
|
}>;
|
|
44
68
|
export type BillingOperationPointer = z.infer<typeof PointerSchema>;
|
|
45
69
|
export declare function operationPointerKey(scope: BillingScope): string;
|
|
@@ -19,7 +19,20 @@ const PointerSchema = z.object({
|
|
|
19
19
|
/** This tab issued the operation and has not yet seen it succeed. */
|
|
20
20
|
awaited: z.literal(true).optional(),
|
|
21
21
|
/** The end this tab saw it reach, kept only by a store that retains settled pointers. */
|
|
22
|
-
settled: z.enum(['succeeded', 'reconciliation_needed']).optional()
|
|
22
|
+
settled: z.enum(['succeeded', 'reconciliation_needed']).optional(),
|
|
23
|
+
/** The hosted step this page redirected to; the page it returns to reports the return. */
|
|
24
|
+
redirect: z
|
|
25
|
+
.object({
|
|
26
|
+
destination: z.enum(['stripe', 'billing_web']),
|
|
27
|
+
step: z.enum([
|
|
28
|
+
'authentication',
|
|
29
|
+
'payment_method',
|
|
30
|
+
'invoice_payment',
|
|
31
|
+
'checkout'
|
|
32
|
+
]),
|
|
33
|
+
method_kind: z.enum(['card', 'alipay', 'other']).optional()
|
|
34
|
+
})
|
|
35
|
+
.optional()
|
|
23
36
|
});
|
|
24
37
|
export function operationPointerKey(scope) {
|
|
25
38
|
return `${POINTER_KEY_PREFIX}:${scope.userId}:${scope.workspaceId}`;
|
|
@@ -40,6 +40,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
40
40
|
duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
|
|
41
41
|
kind: z.ZodEnum<["plan", "promotion"]>;
|
|
42
42
|
name: z.ZodOptional<z.ZodString>;
|
|
43
|
+
term: z.ZodOptional<z.ZodEnum<["this_payment", "first_month", "first_year", "months", "ongoing"]>>;
|
|
43
44
|
} & {
|
|
44
45
|
amount_off_cents: z.ZodOptional<z.ZodNumber>;
|
|
45
46
|
duration_in_months: z.ZodOptional<z.ZodNumber>;
|
|
@@ -50,6 +51,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
50
51
|
amount_off_cents?: number | undefined;
|
|
51
52
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
52
53
|
duration_in_months?: number | undefined;
|
|
54
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
53
55
|
}, {
|
|
54
56
|
code: string;
|
|
55
57
|
kind: "plan" | "promotion";
|
|
@@ -57,10 +59,11 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
57
59
|
amount_off_cents?: number | undefined;
|
|
58
60
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
59
61
|
duration_in_months?: number | undefined;
|
|
62
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
60
63
|
}>>;
|
|
61
64
|
}, "strip", z.ZodTypeAny, {
|
|
62
|
-
amount_cents: number;
|
|
63
65
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
66
|
+
amount_cents: number;
|
|
64
67
|
discount?: {
|
|
65
68
|
code: string;
|
|
66
69
|
kind: "plan" | "promotion";
|
|
@@ -68,10 +71,11 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
68
71
|
amount_off_cents?: number | undefined;
|
|
69
72
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
70
73
|
duration_in_months?: number | undefined;
|
|
74
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
71
75
|
} | undefined;
|
|
72
76
|
}, {
|
|
73
|
-
amount_cents: number;
|
|
74
77
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
78
|
+
amount_cents: number;
|
|
75
79
|
discount?: {
|
|
76
80
|
code: string;
|
|
77
81
|
kind: "plan" | "promotion";
|
|
@@ -79,6 +83,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
79
83
|
amount_off_cents?: number | undefined;
|
|
80
84
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
81
85
|
duration_in_months?: number | undefined;
|
|
86
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
82
87
|
} | undefined;
|
|
83
88
|
}>, "many">;
|
|
84
89
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -86,8 +91,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
86
91
|
amount_charged_cents: number;
|
|
87
92
|
prorated: boolean;
|
|
88
93
|
reasons: {
|
|
89
|
-
amount_cents: number;
|
|
90
94
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
95
|
+
amount_cents: number;
|
|
91
96
|
discount?: {
|
|
92
97
|
code: string;
|
|
93
98
|
kind: "plan" | "promotion";
|
|
@@ -95,6 +100,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
95
100
|
amount_off_cents?: number | undefined;
|
|
96
101
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
97
102
|
duration_in_months?: number | undefined;
|
|
103
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
98
104
|
} | undefined;
|
|
99
105
|
}[];
|
|
100
106
|
}, {
|
|
@@ -102,8 +108,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
102
108
|
amount_charged_cents: number;
|
|
103
109
|
prorated: boolean;
|
|
104
110
|
reasons: {
|
|
105
|
-
amount_cents: number;
|
|
106
111
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
112
|
+
amount_cents: number;
|
|
107
113
|
discount?: {
|
|
108
114
|
code: string;
|
|
109
115
|
kind: "plan" | "promotion";
|
|
@@ -111,6 +117,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
111
117
|
amount_off_cents?: number | undefined;
|
|
112
118
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
113
119
|
duration_in_months?: number | undefined;
|
|
120
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
114
121
|
} | undefined;
|
|
115
122
|
}[];
|
|
116
123
|
}>>;
|
|
@@ -133,8 +140,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
133
140
|
amount_charged_cents: number;
|
|
134
141
|
prorated: boolean;
|
|
135
142
|
reasons: {
|
|
136
|
-
amount_cents: number;
|
|
137
143
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
144
|
+
amount_cents: number;
|
|
138
145
|
discount?: {
|
|
139
146
|
code: string;
|
|
140
147
|
kind: "plan" | "promotion";
|
|
@@ -142,6 +149,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
142
149
|
amount_off_cents?: number | undefined;
|
|
143
150
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
144
151
|
duration_in_months?: number | undefined;
|
|
152
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
145
153
|
} | undefined;
|
|
146
154
|
}[];
|
|
147
155
|
} | undefined;
|
|
@@ -169,8 +177,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
169
177
|
amount_charged_cents: number;
|
|
170
178
|
prorated: boolean;
|
|
171
179
|
reasons: {
|
|
172
|
-
amount_cents: number;
|
|
173
180
|
kind: "promo_code" | "subscription_discount" | "account_balance";
|
|
181
|
+
amount_cents: number;
|
|
174
182
|
discount?: {
|
|
175
183
|
code: string;
|
|
176
184
|
kind: "plan" | "promotion";
|
|
@@ -178,6 +186,7 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
178
186
|
amount_off_cents?: number | undefined;
|
|
179
187
|
duration?: "once" | "repeating" | "forever" | undefined;
|
|
180
188
|
duration_in_months?: number | undefined;
|
|
189
|
+
term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
|
|
181
190
|
} | undefined;
|
|
182
191
|
}[];
|
|
183
192
|
} | undefined;
|
|
@@ -188,6 +197,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
|
|
|
188
197
|
retryable?: boolean | undefined;
|
|
189
198
|
}>;
|
|
190
199
|
export type BillingOpStatus = z.infer<typeof BillingOpStatusSchema>;
|
|
200
|
+
export type BillingChargeBreakdown = NonNullable<BillingOpStatus['charge_breakdown']>;
|
|
201
|
+
export type BillingChargeReason = BillingChargeBreakdown['reasons'][number];
|
|
191
202
|
export type BillingOperationKind = 'subscription' | 'topup' | 'cancel';
|
|
192
203
|
/**
|
|
193
204
|
* Where the customer completes the operation: the challenge this tab drives
|
|
@@ -270,6 +281,7 @@ export type FailedBillingOperation = BillingOperationIdentity & {
|
|
|
270
281
|
*/
|
|
271
282
|
export interface BillingOperationReceipt {
|
|
272
283
|
readonly amountChargedCents?: number;
|
|
284
|
+
readonly chargeBreakdown?: BillingChargeBreakdown;
|
|
273
285
|
readonly creditsAdded?: number;
|
|
274
286
|
readonly plan?: NonNullable<BillingOpStatus['plan']>;
|
|
275
287
|
}
|
|
@@ -11,26 +11,21 @@
|
|
|
11
11
|
* so no server or payment-provider text can reach a consumer through this
|
|
12
12
|
* state.
|
|
13
13
|
*/
|
|
14
|
-
import { zBillingOpChargeBreakdown, zBillingOpChargeReason, zBillingOpStatusResponse
|
|
14
|
+
import { zBillingOpChargeBreakdown, zBillingOpChargeReason, zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { SubscriptionDiscountSchema } from './subscriptionDiscount.js';
|
|
16
17
|
import { wireCents } from './wireCents.js';
|
|
18
|
+
const ChargeBreakdownSchema = zBillingOpChargeBreakdown.extend({
|
|
19
|
+
amount_charged_cents: wireCents,
|
|
20
|
+
reasons: z.array(zBillingOpChargeReason.extend({
|
|
21
|
+
amount_cents: wireCents,
|
|
22
|
+
discount: SubscriptionDiscountSchema.optional()
|
|
23
|
+
}))
|
|
24
|
+
});
|
|
17
25
|
export const BillingOpStatusSchema = zBillingOpStatusResponse.extend({
|
|
18
26
|
amount_charged_cents: wireCents.optional(),
|
|
19
27
|
credits_added: wireCents.optional(),
|
|
20
|
-
charge_breakdown:
|
|
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()
|
|
28
|
+
charge_breakdown: ChargeBreakdownSchema.optional()
|
|
34
29
|
});
|
|
35
30
|
/**
|
|
36
31
|
* The phases the contract defines as blocked on the customer. Neither advances
|
|
@@ -127,6 +122,9 @@ function receiptOf(status) {
|
|
|
127
122
|
...(status.amount_charged_cents === undefined
|
|
128
123
|
? {}
|
|
129
124
|
: { amountChargedCents: status.amount_charged_cents }),
|
|
125
|
+
...(status.charge_breakdown === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { chargeBreakdown: status.charge_breakdown }),
|
|
130
128
|
...(status.credits_added === undefined
|
|
131
129
|
? {}
|
|
132
130
|
: { creditsAdded: status.credits_added }),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BillingDeclineReason, BillingOperationState } from './operationState.js';
|
|
2
|
+
export type PaymentFrictionSignal = {
|
|
3
|
+
readonly stage: 'challenge_required';
|
|
4
|
+
} | {
|
|
5
|
+
readonly stage: 'challenge_completed';
|
|
6
|
+
} | {
|
|
7
|
+
readonly stage: 'challenge_failed';
|
|
8
|
+
readonly declineReason?: BillingDeclineReason;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* What one transition of a pending operation put in the customer's way: a
|
|
12
|
+
* challenge first required under a client secret, this tab's verdict on it,
|
|
13
|
+
* or a retryable decline with a reason the operation did not hold before.
|
|
14
|
+
* The first decline the server reports for a challenge this tab already saw
|
|
15
|
+
* fail is the same failure, so it is not reported twice.
|
|
16
|
+
*/
|
|
17
|
+
export declare function paymentFrictionBetween(before: BillingOperationState | undefined, after: BillingOperationState): readonly PaymentFrictionSignal[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one transition of a pending operation put in the customer's way: a
|
|
3
|
+
* challenge first required under a client secret, this tab's verdict on it,
|
|
4
|
+
* or a retryable decline with a reason the operation did not hold before.
|
|
5
|
+
* The first decline the server reports for a challenge this tab already saw
|
|
6
|
+
* fail is the same failure, so it is not reported twice.
|
|
7
|
+
*/
|
|
8
|
+
export function paymentFrictionBetween(before, after) {
|
|
9
|
+
if (after.phase !== 'pending')
|
|
10
|
+
return [];
|
|
11
|
+
const earlier = before?.phase === 'pending' ? before : undefined;
|
|
12
|
+
return [
|
|
13
|
+
...challengeSignals(earlier?.challenge, after.challenge),
|
|
14
|
+
...declineSignals(earlier, after)
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function challengeSignals(before, after) {
|
|
18
|
+
if (after === undefined)
|
|
19
|
+
return [];
|
|
20
|
+
if (after.clientSecret !== before?.clientSecret) {
|
|
21
|
+
return after.status === 'required' ? [{ stage: 'challenge_required' }] : [];
|
|
22
|
+
}
|
|
23
|
+
if (after.status === before.status)
|
|
24
|
+
return [];
|
|
25
|
+
if (after.status === 'completed')
|
|
26
|
+
return [{ stage: 'challenge_completed' }];
|
|
27
|
+
if (after.status === 'failed')
|
|
28
|
+
return [{ stage: 'challenge_failed' }];
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
function declineSignals(before, after) {
|
|
32
|
+
const reason = after.declineReason;
|
|
33
|
+
if (reason === undefined || reason === before?.declineReason)
|
|
34
|
+
return [];
|
|
35
|
+
const echoesFailedChallenge = after.presentation === 'embedded' &&
|
|
36
|
+
after.challenge?.status === 'failed' &&
|
|
37
|
+
before?.declineReason === undefined;
|
|
38
|
+
return echoesFailedChallenge
|
|
39
|
+
? []
|
|
40
|
+
: [{ stage: 'challenge_failed', declineReason: reason }];
|
|
41
|
+
}
|