Files
Desktop/ts/types/Donations.std.ts
Fedor Indutny 44076ece79 Rename files
2025-10-16 23:45:44 -07:00

196 lines
6.3 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { z } from 'zod';
export const ONE_TIME_DONATION_CONFIG_ID = '1';
export const BOOST_ID = 'BOOST';
export const donationStateSchema = z.enum([
'INTENT',
'INTENT_METHOD',
'INTENT_CONFIRMED',
'INTENT_REDIRECT',
'RECEIPT',
'DONE',
]);
export type DonationStateType = z.infer<typeof donationStateSchema>;
export const donationErrorTypeSchema = z.enum([
// Used if the user is redirected back from validation, but continuing forward fails
'Failed3dsValidation',
// Any other error
'GeneralError',
// Any 4xx error when adding payment method or confirming intent
'PaymentDeclined',
// When it's been too long since the last step of the donation, and card wasn't charged
'TimedOut',
// When donation succeeds but badge application fails
'BadgeApplicationFailed',
]);
export type DonationErrorType = z.infer<typeof donationErrorTypeSchema>;
const coreDataSchema = z.object({
// Guid used to prevent duplicates at stripe and in our db
id: z.string(),
// Currency code, like USD
currencyType: z.string(),
// Cents as whole numbers, so multiply by 100
paymentAmount: z.number(),
// The last time we transitioned into a new state.
timestamp: z.number(),
});
export type CoreData = z.infer<typeof coreDataSchema>;
// Payment type: CARD
export type CardDetail = {
// Two digits
expirationMonth: string;
// Four digts
expirationYear: string;
// String with no separators, just 16 digits
number: string;
// String
cvc: string;
};
const stripeDataSchema = z.object({
// Received after creation of intent
clientSecret: z.string(),
// Parsed out of clientSecret - it's everything up to the '_secret_'
// https://docs.stripe.com/api/payment_intents/object
paymentIntentId: z.string(),
// Used for any validation that takes the user somewhere else
returnToken: z.string(),
});
export type StripeData = z.infer<typeof stripeDataSchema>;
// We need these for durability. if we keep these around throughout the process, retries
// later in the process won't give us weird errors.
// Generated by libsignal.
const receiptContextSchema = z.object({
receiptCredentialRequestContextBase64: z.string(),
receiptCredentialRequestBase64: z.string(),
});
export type ReceiptContext = z.infer<typeof receiptContextSchema>;
export const donationReceiptSchema = z.object({
...coreDataSchema.shape,
});
export type DonationReceipt = z.infer<typeof donationReceiptSchema>;
export const donationWorkflowSchema = z.discriminatedUnion('type', [
z.object({
// Track that user has chosen currency and amount, and we've successfully fetched an
// intent. There is no need to persist this, because we'd need to update
// currency/amount on the intent if we want to continue to use it.
type: z.literal(donationStateSchema.Enum.INTENT),
...coreDataSchema.shape,
...stripeDataSchema.shape,
}),
z.object({
// Once we are here, we can proceed without further user input. The user has entered
// payment details and pressed the button to make the payment, and we have sent that
// to stripe, which has saved that data behind a paymentMethodId. The only thing
// that might require further user interaction: 3ds validation - see INTENT_REDIRECT.
type: z.literal(donationStateSchema.Enum.INTENT_METHOD),
// Stripe persists the user's payment information for us, behind this id
paymentMethodId: z.string(),
...coreDataSchema.shape,
...stripeDataSchema.shape,
}),
z.object({
// By this point, Stripe is attempting to charge the user's provided payment method.
// However it will take some time (usually seconds, sometimes minutes or 1 day) to
// finalize the transaction. We will only know when we successfully get a receipt
// credential from the chat server.
type: z.literal(donationStateSchema.Enum.INTENT_CONFIRMED),
...coreDataSchema.shape,
...stripeDataSchema.shape,
...receiptContextSchema.shape,
}),
z.object({
// An alternate state to INTENT_CONFIRMED. A response from Stripe indicated
// the user's card requires 3ds authentication, so we need to redirect to their
// bank, which will complete verification, then redirect back to us. We hand that
// service a token to connect it back to this process. If the user never comes back,
// we need to offer the redirect again.
type: z.literal(donationStateSchema.Enum.INTENT_REDIRECT),
// Where user should be sent; in this state we are waiting for them to come back
redirectTarget: z.string(),
...coreDataSchema.shape,
...stripeDataSchema.shape,
...receiptContextSchema.shape,
}),
z.object({
// We now have everything we need to redeem. We know the payment has gone through
// successfully; we just need to redeem it on the server anonymously.
type: z.literal(donationStateSchema.Enum.RECEIPT),
// The result of mixing the receiptCredentialResponse from the API from our
// previously-generated receiptCredentialRequestContext
receiptCredentialBase64: z.string(),
...coreDataSchema.shape,
}),
z.object({
// After everything is done, we should notify the user the donation succeeded.
// After we show a notification, or if the user initiates a new donation,
// then this workflow can be deleted.
type: z.literal(donationStateSchema.Enum.DONE),
id: coreDataSchema.shape.id,
timestamp: coreDataSchema.shape.timestamp,
}),
]);
export type DonationWorkflow = z.infer<typeof donationWorkflowSchema>;
export const humanDonationAmountSchema = z
.number()
.nonnegative()
.brand('humanAmount');
export type HumanDonationAmount = z.infer<typeof humanDonationAmountSchema>;
// Always in currency minor units e.g. 1000 for 10 USD, 10 for 10 JPY
// https://docs.stripe.com/currencies#minor-units
export const stripeDonationAmountSchema = z
.number()
.nonnegative()
.brand('stripeAmount');
export type StripeDonationAmount = z.infer<typeof stripeDonationAmountSchema>;
export const subscriptionConfigurationCurrencyZod = z.object({
minimum: humanDonationAmountSchema,
oneTime: z.record(z.string(), humanDonationAmountSchema.array()),
});
export const oneTimeDonationAmountsZod = z.record(
z.string(),
subscriptionConfigurationCurrencyZod
);
export type OneTimeDonationHumanAmounts = z.infer<
typeof oneTimeDonationAmountsZod
>;