import { type DoneActorEvent } from "xstate"; import { applyPromotionUnlockOutput } from "../helper/promotion"; import { applySingleUnlockOutput, countLockedHistoryMessages, type UnlockMessageOutput, } from "../helper/unlock"; import type { UnlockHistoryOutput } from "./actors/unlock"; import { sendMachineSetup } from "./send-flow"; import { createChatActorActionSetup } from "./setup"; import { getCharacterErrorCode } from "@/data/services/api"; const markPaymentUnlockPendingAction = sendMachineSetup.assign(() => ({ paymentUnlockPending: true, unlockHistoryError: null, })); const showUnlockHistoryPromptAction = sendMachineSetup.assign( ({ context }) => ({ paymentUnlockPending: false, unlockHistoryPromptVisible: true, lockedHistoryCount: countLockedHistoryMessages(context.messages), unlockHistoryError: null, unlockHistoryPaymentGuidance: null, }), ); const dismissUnlockHistoryPromptAction = sendMachineSetup.assign(() => ({ paymentUnlockPending: false, unlockHistoryPromptVisible: false, unlockHistoryError: null, unlockHistoryPaymentGuidance: null, })); const markUnlockHistoryStartedAction = sendMachineSetup.assign(() => ({ paymentUnlockPending: false, unlockHistoryPromptVisible: false, unlockHistoryError: null, unlockHistoryPaymentGuidance: null, })); const markUnlockHistoryFailedAction = sendMachineSetup.assign(() => ({ unlockHistoryPromptVisible: true, unlockHistoryError: "Failed to unlock messages. Please try again.", })); const markUnlockMessageStartedAction = sendMachineSetup.assign( ({ event }) => { if (event.type !== "ChatUnlockMessageRequested") return {}; return { unlockingMessage: { displayMessageId: event.displayMessageId, ...(event.remoteMessageId ? { messageId: event.remoteMessageId } : {}), kind: event.kind, ...(event.lockType ? { lockType: event.lockType } : {}), ...(event.clientLockId ? { clientLockId: event.clientLockId } : {}), }, unlockMessageError: null, unlockPaywallRequest: null, }; }, ); const applyUnlockMessageSucceededAction = sendMachineSetup.assign( ({ context, event }) => { const { output } = event as unknown as DoneActorEvent; return { messages: applySingleUnlockOutput(context.messages, output), promotion: applyPromotionUnlockOutput(context.promotion, output), unlockingMessage: null, unlockMessageError: null, unlockPaywallRequest: null, creditBalance: output.response.creditBalance, creditsCharged: output.response.creditsCharged, requiredCredits: 0, shortfallCredits: 0, paymentGuidance: null, }; }, ); const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign( ({ context, event }) => { const { output } = event as unknown as DoneActorEvent; const shouldOpenPaywall = output.response.reason !== "not_found"; return { promotion: applyPromotionUnlockOutput(context.promotion, output), unlockingMessage: null, unlockMessageError: output.response.reason, unlockPaywallRequest: shouldOpenPaywall ? { displayMessageId: output.displayMessageId, ...(output.response.messageId || output.request.messageId ? { messageId: output.response.messageId || output.request.messageId, } : {}), kind: context.unlockingMessage?.kind ?? "private", ...(output.request.lockType ? { lockType: output.request.lockType } : {}), ...(output.request.clientLockId ? { clientLockId: output.request.clientLockId } : {}), ...(context.promotion?.message.displayId === output.displayMessageId ? { promotion: context.promotion.session } : {}), reason: output.response.reason, creditBalance: output.response.creditBalance, requiredCredits: output.response.requiredCredits, shortfallCredits: output.response.shortfallCredits, ...(output.response.paymentGuidance ? { paymentGuidance: output.response.paymentGuidance } : {}), } : null, creditBalance: output.response.creditBalance, requiredCredits: output.response.requiredCredits, shortfallCredits: output.response.shortfallCredits, paymentGuidance: output.response.paymentGuidance, }; }, ); const requestUnlockPaymentFromErrorAction = sendMachineSetup.assign( ({ context }) => { const request = context.unlockingMessage; return { unlockingMessage: null, unlockMessageError: "unlock_failed", unlockPaywallRequest: request ? { ...request, ...(context.promotion?.message.displayId === request.displayMessageId ? { promotion: context.promotion.session } : {}), reason: "unlock_failed", creditBalance: context.creditBalance, requiredCredits: context.requiredCredits, shortfallCredits: context.shortfallCredits, } : null, }; }, ); const clearUnlockPaywallRequestAction = sendMachineSetup.assign(() => ({ unlockPaywallRequest: null, })); const handleCharacterMismatchAction = sendMachineSetup.assign(() => ({ unlockingMessage: null, unlockMessageError: "character_mismatch", unlockPaywallRequest: null, characterErrorCode: "CHARACTER_MISMATCH" as const, })); export const unlockMachineSetup = sendMachineSetup.extend({ actions: { markPaymentUnlockPending: markPaymentUnlockPendingAction, showUnlockHistoryPrompt: showUnlockHistoryPromptAction, dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction, markUnlockHistoryStarted: markUnlockHistoryStartedAction, markUnlockHistoryFailed: markUnlockHistoryFailedAction, markUnlockMessageStarted: markUnlockMessageStartedAction, applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction, requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction, requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction, clearUnlockPaywallRequest: clearUnlockPaywallRequestAction, handleCharacterMismatch: handleCharacterMismatchAction, }, }); const unlockHistoryDoneActionSetup = createChatActorActionSetup>(); const applyUnlockHistoryOutputAction = unlockHistoryDoneActionSetup.assign(({ event }) => { const lockedHistoryCount = countLockedHistoryMessages( event.output.messages, ); const insufficientBalance = !event.output.unlocked && event.output.reason === "insufficient_balance"; return { messages: event.output.messages, historyLoaded: true, paymentUnlockPending: false, unlockHistoryPromptVisible: insufficientBalance, lockedHistoryCount, unlockHistoryError: insufficientBalance ? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.` : null, unlockHistoryPaymentGuidance: insufficientBalance ? event.output.paymentGuidance ?? null : null, }; }); export const unlockingHistoryState = unlockMachineSetup.createStateConfig({ invoke: { id: "unlockHistory", src: "unlockHistory", input: ({ context }) => ({ characterId: context.characterId, emptyChatGreeting: context.emptyChatGreeting, }), onDone: { target: "ready", actions: applyUnlockHistoryOutputAction, }, onError: { target: "ready", actions: "markUnlockHistoryFailed", }, }, }); export const unlockingMessageState = unlockMachineSetup.createStateConfig({ invoke: { id: "unlockMessage", src: "unlockMessage", input: ({ context }) => ({ characterId: context.characterId, ...(context.unlockingMessage ?? { displayMessageId: "", kind: "private" as const, }), }), onDone: [ { guard: ({ event }) => event.output.response.unlocked, target: "ready", actions: "applyUnlockMessageSucceeded", }, { target: "ready", actions: "requestUnlockPaymentFromOutput", }, ], onError: [ { guard: ({ event }) => getCharacterErrorCode(event.error) === "CHARACTER_MISMATCH", target: "ready", actions: "handleCharacterMismatch", }, { target: "ready", actions: "requestUnlockPaymentFromError", }, ], }, });