feat(tip): add personalized payment success experience
This commit is contained in:
@@ -101,6 +101,10 @@ export function createTestPaymentMachine(
|
||||
createOrderError: Error;
|
||||
orderStatus: "pending" | "paid" | "failed";
|
||||
orderStatuses: ("pending" | "paid" | "failed")[];
|
||||
orderType: string;
|
||||
orderPlanId: string;
|
||||
tipCount: number | null;
|
||||
thankYouMessage: string | null;
|
||||
}> = {},
|
||||
) {
|
||||
const createOrderSpy = overrides.createOrderSpy ?? vi.fn<CreateOrderSpy>();
|
||||
@@ -150,8 +154,10 @@ export function createTestPaymentMachine(
|
||||
return PaymentOrderStatusResponseSchema.parse({
|
||||
orderId: "pay_test_001",
|
||||
status,
|
||||
orderType: "vip_monthly",
|
||||
planId: "vip_monthly",
|
||||
orderType: overrides.orderType ?? "vip_monthly",
|
||||
planId: overrides.orderPlanId ?? "vip_monthly",
|
||||
tipCount: overrides.tipCount ?? null,
|
||||
thankYouMessage: overrides.thankYouMessage ?? null,
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -65,6 +65,8 @@ describe("payment order flow", () => {
|
||||
expect(actor.getSnapshot().context).toMatchObject({
|
||||
currentOrderId: "pay_test_001",
|
||||
orderStatus: "paid",
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: null,
|
||||
launchNonce: 1,
|
||||
});
|
||||
@@ -79,6 +81,80 @@ describe("payment order flow", () => {
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("stores a stable Tip success result and clears it on reset", async () => {
|
||||
const actor = createActor(
|
||||
createTestPaymentMachine({
|
||||
orderType: "tip",
|
||||
orderPlanId: "tip_coffee_usd_9_99",
|
||||
tipCount: 2,
|
||||
thankYouMessage: "You made my day.",
|
||||
}),
|
||||
).start();
|
||||
await initialize(actor);
|
||||
actor.send({
|
||||
type: "PaymentCreateOrderSubmitted",
|
||||
recipientCharacterId: "maya-tan",
|
||||
});
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("paid"));
|
||||
|
||||
expect(actor.getSnapshot().context).toMatchObject({
|
||||
orderStatus: "paid",
|
||||
tipCount: 2,
|
||||
thankYouMessage: "You made my day.",
|
||||
});
|
||||
|
||||
actor.send({ type: "PaymentReset" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
expect(actor.getSnapshot().context).toMatchObject({
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
});
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("clears the previous Tip result before creating another order", async () => {
|
||||
const actor = createActor(
|
||||
createTestPaymentMachine({
|
||||
orderType: "tip",
|
||||
tipCount: 3,
|
||||
thankYouMessage: "Another warm coffee.",
|
||||
}),
|
||||
).start();
|
||||
await initialize(actor);
|
||||
actor.send({ type: "PaymentCreateOrderSubmitted" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("paid"));
|
||||
|
||||
actor.send({ type: "PaymentCreateOrderSubmitted" });
|
||||
expect(actor.getSnapshot().matches("creatingOrder")).toBe(true);
|
||||
expect(actor.getSnapshot().context).toMatchObject({
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
});
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("clears the previous Tip result when the payment channel changes", async () => {
|
||||
const actor = createActor(
|
||||
createTestPaymentMachine({
|
||||
orderType: "tip",
|
||||
tipCount: 4,
|
||||
thankYouMessage: "That was so thoughtful.",
|
||||
}),
|
||||
).start();
|
||||
await initialize(actor);
|
||||
actor.send({ type: "PaymentCreateOrderSubmitted" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("paid"));
|
||||
|
||||
actor.send({ type: "PaymentPayChannelChanged", payChannel: "ezpay" });
|
||||
expect(actor.getSnapshot().matches("ready")).toBe(true);
|
||||
expect(actor.getSnapshot().context).toMatchObject({
|
||||
payChannel: "ezpay",
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
});
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("passes the selected tip recipient to order creation", async () => {
|
||||
const createOrderSpy = vi.fn<CreateOrderSpy>();
|
||||
const actor = createActor(
|
||||
@@ -135,6 +211,8 @@ describe("payment order flow", () => {
|
||||
currentOrderId: null,
|
||||
payParams: null,
|
||||
orderStatus: null,
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: null,
|
||||
errorMessage: null,
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ export function resetOrderState(): Partial<PaymentState> {
|
||||
currentOrderPlanId: null,
|
||||
payParams: null,
|
||||
orderStatus: null,
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ const applyReturnedOrderAction = catalogMachineSetup.assign(({ event }) => {
|
||||
currentOrderPlanId: null,
|
||||
payParams: null,
|
||||
orderStatus: "pending",
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: event.createdAt ?? Date.now(),
|
||||
errorMessage: null,
|
||||
};
|
||||
@@ -97,6 +99,8 @@ const applyCreateOrderSuccessAction = createOrderDoneSetup.assign(
|
||||
currentOrderPlanId: context.selectedPlanId,
|
||||
payParams: event.output.payParams,
|
||||
orderStatus: "pending",
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: Date.now(),
|
||||
errorMessage: null,
|
||||
launchNonce: context.launchNonce + 1,
|
||||
@@ -134,6 +138,8 @@ const trackPollTimeoutAction = pollOrderDoneSetup.createAction(
|
||||
|
||||
const applyPaidOrderAction = pollOrderDoneSetup.assign(({ event }) => ({
|
||||
orderStatus: event.output.status,
|
||||
tipCount: event.output.tipCount,
|
||||
thankYouMessage: event.output.thankYouMessage,
|
||||
orderPollingStartedAt: null,
|
||||
errorMessage: null,
|
||||
}));
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface PaymentContextState {
|
||||
currentOrderId: string | null;
|
||||
payParams: Record<string, unknown> | null;
|
||||
orderStatus: MachineContext["orderStatus"];
|
||||
tipCount: number | null;
|
||||
thankYouMessage: string | null;
|
||||
errorMessage: string | null;
|
||||
launchNonce: number;
|
||||
isLoadingPlans: boolean;
|
||||
@@ -72,6 +74,8 @@ function selectPaymentState(state: PaymentSnapshot): PaymentContextState {
|
||||
currentOrderId: state.context.currentOrderId,
|
||||
payParams: state.context.payParams,
|
||||
orderStatus: state.context.orderStatus,
|
||||
tipCount: state.context.tipCount,
|
||||
thankYouMessage: state.context.thankYouMessage,
|
||||
errorMessage: state.context.errorMessage,
|
||||
launchNonce: state.context.launchNonce,
|
||||
isLoadingPlans:
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface PaymentState {
|
||||
currentOrderPlanId: string | null;
|
||||
payParams: Record<string, unknown> | null;
|
||||
orderStatus: PaymentOrderStatus | null;
|
||||
tipCount: number | null;
|
||||
thankYouMessage: string | null;
|
||||
orderPollingStartedAt: number | null;
|
||||
errorMessage: string | null;
|
||||
launchNonce: number;
|
||||
@@ -38,6 +40,8 @@ export const initialState: PaymentState = {
|
||||
currentOrderPlanId: null,
|
||||
payParams: null,
|
||||
orderStatus: null,
|
||||
tipCount: null,
|
||||
thankYouMessage: null,
|
||||
orderPollingStartedAt: null,
|
||||
errorMessage: null,
|
||||
launchNonce: 0,
|
||||
|
||||
Reference in New Issue
Block a user