@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.
Files changed (69) hide show
  1. package/dist/core/billing/index.d.ts +4 -3
  2. package/dist/core/billing/index.js +2 -1
  3. package/dist/core/billing/operationLifecycle.d.ts +26 -4
  4. package/dist/core/billing/operationLifecycle.js +113 -14
  5. package/dist/core/billing/operationPointer.d.ts +24 -0
  6. package/dist/core/billing/operationPointer.js +14 -1
  7. package/dist/core/billing/operationState.d.ts +18 -6
  8. package/dist/core/billing/operationState.js +13 -15
  9. package/dist/core/billing/paymentFriction.d.ts +17 -0
  10. package/dist/core/billing/paymentFriction.js +41 -0
  11. package/dist/core/billing/subscriptionCommands.d.ts +38 -2
  12. package/dist/core/billing/subscriptionCommands.js +27 -7
  13. package/dist/core/billing/subscriptionDiscount.d.ts +26 -0
  14. package/dist/core/billing/subscriptionDiscount.js +6 -0
  15. package/dist/core/billing/telemetry/billingTelemetryEvent.d.ts +25 -0
  16. package/dist/core/billing/telemetry/billingTelemetryEvent.js +3 -0
  17. package/dist/core/billing/telemetry/cancelEvent.d.ts +19 -0
  18. package/dist/core/billing/telemetry/cancelEvent.js +1 -0
  19. package/dist/core/billing/telemetry/capabilityReadEvent.d.ts +4 -0
  20. package/dist/core/billing/telemetry/capabilityReadEvent.js +1 -0
  21. package/dist/core/billing/telemetry/checkoutChallengeEvent.d.ts +25 -0
  22. package/dist/core/billing/telemetry/checkoutChallengeEvent.js +1 -0
  23. package/dist/core/billing/telemetry/checkoutJourney.d.ts +176 -0
  24. package/dist/core/billing/telemetry/checkoutJourney.js +115 -0
  25. package/dist/core/billing/telemetry/checkoutRedirectEvent.d.ts +30 -0
  26. package/dist/core/billing/telemetry/checkoutRedirectEvent.js +1 -0
  27. package/dist/core/billing/telemetry/downgradeToPersonalEvent.d.ts +14 -0
  28. package/dist/core/billing/telemetry/downgradeToPersonalEvent.js +1 -0
  29. package/dist/core/billing/telemetry/entryEvent.d.ts +16 -0
  30. package/dist/core/billing/telemetry/entryEvent.js +1 -0
  31. package/dist/core/billing/telemetry/eventNames.d.ts +36 -0
  32. package/dist/core/billing/telemetry/eventNames.js +36 -0
  33. package/dist/core/billing/telemetry/index.d.ts +17 -0
  34. package/dist/core/billing/telemetry/index.js +6 -0
  35. package/dist/core/billing/telemetry/operationEvent.d.ts +25 -0
  36. package/dist/core/billing/telemetry/operationEvent.js +1 -0
  37. package/dist/core/billing/telemetry/operationLifecycleEvent.d.ts +9 -0
  38. package/dist/core/billing/telemetry/operationLifecycleEvent.js +79 -0
  39. package/dist/core/billing/telemetry/payload.d.ts +55 -0
  40. package/dist/core/billing/telemetry/payload.js +75 -0
  41. package/dist/core/billing/telemetry/portalEvent.d.ts +23 -0
  42. package/dist/core/billing/telemetry/portalEvent.js +1 -0
  43. package/dist/core/billing/telemetry/resubscribeEvent.d.ts +13 -0
  44. package/dist/core/billing/telemetry/resubscribeEvent.js +1 -0
  45. package/dist/core/billing/telemetry/stages.d.ts +45 -0
  46. package/dist/core/billing/telemetry/stages.js +1 -0
  47. package/dist/core/billing/telemetry/subscriptionCheckoutEvent.d.ts +23 -0
  48. package/dist/core/billing/telemetry/subscriptionCheckoutEvent.js +1 -0
  49. package/dist/core/billing/telemetry/topupEvent.d.ts +30 -0
  50. package/dist/core/billing/telemetry/topupEvent.js +6 -0
  51. package/dist/core/billing/telemetry/vocabulary.d.ts +9 -0
  52. package/dist/core/billing/telemetry/vocabulary.js +1 -0
  53. package/dist/core/billing/telemetry/webEntryEvent.d.ts +27 -0
  54. package/dist/core/billing/telemetry/webEntryEvent.js +1 -0
  55. package/dist/core/billing/telemetry/webHandoffEvent.d.ts +11 -0
  56. package/dist/core/billing/telemetry/webHandoffEvent.js +1 -0
  57. package/dist/core/billing/telemetry/webReturnEvent.d.ts +8 -0
  58. package/dist/core/billing/telemetry/webReturnEvent.js +1 -0
  59. package/dist/core/billing/telemetry/webSessionEvent.d.ts +17 -0
  60. package/dist/core/billing/telemetry/webSessionEvent.js +1 -0
  61. package/dist/core/webSession.d.ts +4 -3
  62. package/dist/core/webSession.js +6 -5
  63. package/dist/core/webSessionIdentity.d.ts +6 -6
  64. package/dist/signInSchemas.d.ts +1 -1
  65. package/dist/telemetry.d.ts +26 -0
  66. package/dist/telemetry.js +28 -7
  67. package/dist/testing.d.ts +2 -1
  68. package/dist/testing.js +5 -4
  69. 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
- export interface BillingOperationTelemetryEvent {
36
- readonly name: (typeof BILLING_OPERATION_TELEMETRY_EVENT)[keyof typeof BILLING_OPERATION_TELEMETRY_EVENT];
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 next = reduceBillingOperation(record.state, event);
160
- if (next === record.state)
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 writePointer(scope, state) {
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
- presentation: pointer.presentation,
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
- presentation: known?.presentation ?? routeFor(rail, pending),
474
- attemptStartedAt: known?.attemptStartedAt ?? now(),
475
- resumed: true,
476
- awaitedHere: known?.awaited === true
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, zSubscriptionDiscount } from '@comfyorg/ingest-types/zod';
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: 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()
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
+ }