mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 12:19:41 +00:00
196 lines
6.3 KiB
TypeScript
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
|
|
>;
|