mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(premium): add App Store and Google Play purchases (#3052)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SnowflakeStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const AdminStorePurchaseResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier (snowflake) for this store purchase'),
|
||||
user_id: SnowflakeStringType.nullable().describe('ID of the user the purchase is bound to, null when unbound'),
|
||||
provider: z.enum(['app_store', 'google_play']).describe('Store the purchase was made in'),
|
||||
kind: z.enum(['subscription', 'gift']).describe('Whether the purchase is a subscription or a gift'),
|
||||
slot: z.enum(['monthly', 'yearly', 'gift_1_month', 'gift_1_year']).describe('Fluxer product the purchase is for'),
|
||||
environment: z.enum(['production', 'sandbox']).describe('Whether the purchase was real or a test purchase'),
|
||||
app_id: z.string().describe('Bundle identifier or package name of the app that made the purchase'),
|
||||
product_id: z.string().describe('Store product identifier'),
|
||||
base_plan_id: z.string().nullable().describe('Google Play base plan identifier, null otherwise'),
|
||||
latest_transaction_id: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('Latest App Store transaction ID or Google Play order ID for the purchase'),
|
||||
ownership_type: z.string().nullable().describe('Store ownership type, such as PURCHASED or FAMILY_SHARED'),
|
||||
state: z.string().describe('Normalized state of the purchase'),
|
||||
store_state: z.string().nullable().describe('Raw state reported by the store'),
|
||||
entitled: z.boolean().describe('Whether the purchase currently grants Plutonium'),
|
||||
expires_at: z.iso.datetime().nullable().describe('When the paid period ends'),
|
||||
grace_ends_at: z.iso.datetime().nullable().describe('When the billing grace period ends'),
|
||||
auto_renew: z.boolean().nullable().describe('Whether the subscription renews automatically'),
|
||||
started_at: z.iso.datetime().nullable().describe('When the subscription or purchase started'),
|
||||
purchased_at: z.iso.datetime().nullable().describe('When the latest payment was made'),
|
||||
revoked_at: z.iso.datetime().nullable().describe('When the store revoked or refunded the purchase'),
|
||||
revocation_reason: z.string().nullable().describe('Reason the store gave for the revocation'),
|
||||
superseded: z.boolean().describe('Whether a newer purchase replaced this one'),
|
||||
acknowledged: z.boolean().describe('Whether the purchase was acknowledged with the store'),
|
||||
gift_code: z.string().nullable().describe('Gift code minted by a gift purchase'),
|
||||
bound_at: z.iso.datetime().nullable().describe('When the purchase was bound to its user'),
|
||||
last_event_at: z.iso.datetime().nullable().describe('Time of the latest store event applied to the purchase'),
|
||||
synced_at: z.iso.datetime().nullable().describe('When the purchase was last synced with the store'),
|
||||
created_at: z.iso.datetime().describe('When Fluxer first saw the purchase'),
|
||||
updated_at: z.iso.datetime().describe('When the purchase record last changed'),
|
||||
});
|
||||
|
||||
export type AdminStorePurchaseResponse = z.infer<typeof AdminStorePurchaseResponse>;
|
||||
|
||||
export const AdminStorePurchaseListResponse = z.object({
|
||||
purchases: z.array(AdminStorePurchaseResponse).describe('Store purchases bound to the user'),
|
||||
});
|
||||
|
||||
export type AdminStorePurchaseListResponse = z.infer<typeof AdminStorePurchaseListResponse>;
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createStringType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {PremiumStoreSubscriptionState} from '@fluxer/schema/src/domains/premium/StoreBillingSchemas';
|
||||
import {createStringType, withOpenApiType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {UserPremiumTypesSchema} from '@fluxer/schema/src/primitives/UserSettingsValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
@@ -362,11 +363,24 @@ export const PremiumPricingState = z.object({
|
||||
|
||||
export type PremiumPricingState = z.infer<typeof PremiumPricingState>;
|
||||
|
||||
export const PremiumSubscriptionProvider = withOpenApiType(
|
||||
z.enum(['stripe', 'app_store', 'google_play']),
|
||||
'PremiumSubscriptionProvider',
|
||||
);
|
||||
|
||||
export type PremiumSubscriptionProvider = z.infer<typeof PremiumSubscriptionProvider>;
|
||||
|
||||
export const PremiumStateResponse = z.object({
|
||||
actual: PremiumActualState,
|
||||
effective: PremiumEffectiveState,
|
||||
billing: PremiumBillingState,
|
||||
pricing: PremiumPricingState,
|
||||
store: PremiumStoreSubscriptionState.nullish().describe(
|
||||
'Active App Store or Google Play subscription, null when no store subscription is active',
|
||||
),
|
||||
subscription_provider: PremiumSubscriptionProvider.nullable().describe(
|
||||
'Billing platform that owns the current recurring subscription, null for gift, lifetime or no subscription. When a Stripe and a store subscription are both active, the one paid through later',
|
||||
),
|
||||
});
|
||||
|
||||
export type PremiumStateResponse = z.infer<typeof PremiumStateResponse>;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
createStringType,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
withOpenApiType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
|
||||
const StoreProviderSchema = withOpenApiType(z.enum(['app_store', 'google_play']), 'StoreProvider');
|
||||
|
||||
const StoreSlotSchema = withOpenApiType(z.enum(['monthly', 'yearly', 'gift_1_month', 'gift_1_year']), 'StoreSlot');
|
||||
|
||||
const StorePurchaseKindSchema = withOpenApiType(z.enum(['subscription', 'gift']), 'StorePurchaseKind');
|
||||
|
||||
const StoreEnvironmentSchema = withOpenApiType(z.enum(['production', 'sandbox']), 'StoreEnvironment');
|
||||
|
||||
const StorePurchaseStateSchema = withOpenApiType(
|
||||
z.enum([
|
||||
'pending',
|
||||
'active',
|
||||
'grace',
|
||||
'billing_retry',
|
||||
'on_hold',
|
||||
'paused',
|
||||
'canceled',
|
||||
'expired',
|
||||
'revoked',
|
||||
'superseded',
|
||||
'purchased',
|
||||
'fulfilled',
|
||||
'refunded',
|
||||
]),
|
||||
'StorePurchaseState',
|
||||
);
|
||||
|
||||
const StorePurchaseBlockedReasonSchema = withOpenApiType(
|
||||
z.enum(['lifetime', 'existing_subscription', 'purchase_disabled']),
|
||||
'StorePurchaseBlockedReason',
|
||||
);
|
||||
|
||||
const StoreBlockingProviderSchema = withOpenApiType(
|
||||
z.enum(['stripe', 'app_store', 'google_play']),
|
||||
'StoreBlockingProvider',
|
||||
);
|
||||
|
||||
const StoreBillingCycleSchema = withOpenApiType(z.enum(['monthly', 'yearly']), 'StoreBillingCycle');
|
||||
|
||||
const StoreSubscriptionSlotSchema = withOpenApiType(z.enum(['monthly', 'yearly']), 'StoreSubscriptionSlot');
|
||||
|
||||
export const StoreBillingAppStoreProductResponse = z.object({
|
||||
product_id: z.string().describe('App Store product identifier'),
|
||||
slot: StoreSlotSchema.describe('Fluxer product this App Store product sells'),
|
||||
});
|
||||
|
||||
export type StoreBillingAppStoreProductResponse = z.infer<typeof StoreBillingAppStoreProductResponse>;
|
||||
|
||||
export const StoreBillingGooglePlayProductResponse = z.object({
|
||||
product_id: z.string().describe('Google Play product identifier'),
|
||||
base_plan_id: z.string().nullable().describe('Google Play base plan identifier, null for one-time products'),
|
||||
slot: StoreSlotSchema.describe('Fluxer product this Google Play product sells'),
|
||||
});
|
||||
|
||||
export type StoreBillingGooglePlayProductResponse = z.infer<typeof StoreBillingGooglePlayProductResponse>;
|
||||
|
||||
export const StoreBillingContextResponse = z.object({
|
||||
app_account_token: z
|
||||
.string()
|
||||
.describe(
|
||||
'Lowercase UUID for this account. Pass it as appAccountToken on App Store purchases and as obfuscatedAccountId on Google Play purchases',
|
||||
),
|
||||
app_store: z
|
||||
.object({
|
||||
enabled: z.boolean().describe('Whether App Store purchases are accepted'),
|
||||
bundle_ids: z.array(z.string()).describe('App bundle identifiers whose purchases are accepted'),
|
||||
products: z.array(StoreBillingAppStoreProductResponse).describe('App Store products on sale'),
|
||||
})
|
||||
.describe('App Store purchase settings'),
|
||||
google_play: z
|
||||
.object({
|
||||
enabled: z.boolean().describe('Whether Google Play purchases are accepted'),
|
||||
package_names: z.array(z.string()).describe('App package names whose purchases are accepted'),
|
||||
products: z.array(StoreBillingGooglePlayProductResponse).describe('Google Play products on sale'),
|
||||
})
|
||||
.describe('Google Play purchase settings'),
|
||||
purchase_blocked_reason: StorePurchaseBlockedReasonSchema.nullable().describe(
|
||||
'Why this account cannot buy a subscription right now, null when it can',
|
||||
),
|
||||
blocking_provider: StoreBlockingProviderSchema.nullable().describe(
|
||||
'Provider of the active subscription that blocks a new one, null when nothing blocks',
|
||||
),
|
||||
});
|
||||
|
||||
export type StoreBillingContextResponse = z.infer<typeof StoreBillingContextResponse>;
|
||||
|
||||
export const ClaimAppStoreTransactionRequest = z.object({
|
||||
signed_transaction: createStringType(1, 32768).describe(
|
||||
'JWS signed transaction from StoreKit 2 (Transaction.jwsRepresentation)',
|
||||
),
|
||||
});
|
||||
|
||||
export type ClaimAppStoreTransactionRequest = z.infer<typeof ClaimAppStoreTransactionRequest>;
|
||||
|
||||
export const ClaimGooglePlayPurchaseRequest = z.object({
|
||||
purchase_token: createStringType(1, 1024).describe('Purchase token from Google Play Billing'),
|
||||
product_id: createStringType(1, 256).describe('Google Play product identifier of the purchase'),
|
||||
package_name: createStringType(1, 256)
|
||||
.optional()
|
||||
.describe('Package name of the app that made the purchase. Must be one of the accepted package names'),
|
||||
});
|
||||
|
||||
export type ClaimGooglePlayPurchaseRequest = z.infer<typeof ClaimGooglePlayPurchaseRequest>;
|
||||
|
||||
export const StorePurchaseResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier (snowflake) for this store purchase'),
|
||||
provider: StoreProviderSchema.describe('Store the purchase was made in'),
|
||||
kind: StorePurchaseKindSchema.describe('Whether the purchase is a subscription or a gift'),
|
||||
slot: StoreSlotSchema.describe('Fluxer product the purchase is for'),
|
||||
product_id: z.string().describe('Store product identifier'),
|
||||
environment: StoreEnvironmentSchema.describe('Whether the purchase was a real purchase or a test purchase'),
|
||||
state: StorePurchaseStateSchema.describe('Current state of the purchase'),
|
||||
entitled: z.boolean().describe('Whether the purchase currently grants Plutonium'),
|
||||
expires_at: z.iso.datetime().nullable().describe('When the paid period ends, null for gifts'),
|
||||
entitled_until: z.iso
|
||||
.datetime()
|
||||
.nullable()
|
||||
.describe('When access from this purchase ends, including any grace period, null when not entitled'),
|
||||
will_renew: z.boolean().nullable().describe('Whether the subscription renews automatically, null for gifts'),
|
||||
gift_code: z.string().nullable().describe('Gift code minted by a gift purchase, null otherwise'),
|
||||
created_at: z.iso.datetime().describe('When Fluxer first saw the purchase'),
|
||||
});
|
||||
|
||||
export type StorePurchaseResponse = z.infer<typeof StorePurchaseResponse>;
|
||||
|
||||
export const StorePurchaseClaimResponse = z.object({
|
||||
purchase: StorePurchaseResponse.describe('The claimed purchase'),
|
||||
gift_code: z.string().nullable().describe('Gift code minted by a gift purchase, null otherwise'),
|
||||
});
|
||||
|
||||
export type StorePurchaseClaimResponse = z.infer<typeof StorePurchaseClaimResponse>;
|
||||
|
||||
export const StorePurchaseListResponse = z.array(StorePurchaseResponse);
|
||||
|
||||
export type StorePurchaseListResponse = z.infer<typeof StorePurchaseListResponse>;
|
||||
|
||||
export const StorePurchaseIdParam = z.object({
|
||||
purchase_id: SnowflakeType.describe('The ID of the store purchase'),
|
||||
});
|
||||
|
||||
export type StorePurchaseIdParam = z.infer<typeof StorePurchaseIdParam>;
|
||||
|
||||
export const PremiumStoreSubscriptionState = z.object({
|
||||
provider: StoreProviderSchema.describe('Store that bills the subscription'),
|
||||
purchase_id: SnowflakeStringType.describe('ID of the store purchase that grants the subscription'),
|
||||
slot: StoreSubscriptionSlotSchema.describe('Fluxer subscription product'),
|
||||
billing_cycle: StoreBillingCycleSchema.describe('Billing cycle of the subscription'),
|
||||
state: StorePurchaseStateSchema.describe('Current state of the store subscription'),
|
||||
expires_at: z.iso.datetime().describe('When the paid period ends'),
|
||||
grace_ends_at: z.iso.datetime().nullable().describe('When the billing grace period ends, null outside grace'),
|
||||
will_renew: z.boolean().describe('Whether the subscription renews automatically'),
|
||||
manage_url: z.string().describe('Store page where the subscription can be managed or canceled'),
|
||||
environment: StoreEnvironmentSchema.describe('Whether the subscription is a real purchase or a test purchase'),
|
||||
});
|
||||
|
||||
export type PremiumStoreSubscriptionState = z.infer<typeof PremiumStoreSubscriptionState>;
|
||||
Reference in New Issue
Block a user