mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-24 05:07:06 +08:00
feat: improve sub sync (#13932)
This commit is contained in:
@@ -112,6 +112,10 @@ export const Subscription = z.object({
|
|||||||
duration: z.string().nullable(),
|
duration: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const IdentifyUserResponse = z.object({
|
||||||
|
was_created: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
export type Subscription = z.infer<typeof Subscription>;
|
export type Subscription = z.infer<typeof Subscription>;
|
||||||
type Entitlement = z.infer<typeof zRcV2RawEntitlementItem>;
|
type Entitlement = z.infer<typeof zRcV2RawEntitlementItem>;
|
||||||
type Product = z.infer<typeof zRcV2RawProduct>;
|
type Product = z.infer<typeof zRcV2RawProduct>;
|
||||||
@@ -139,6 +143,41 @@ export class RevenueCatService {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async identifyUser(userId: string, newUserId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.revenuecat.com/v1/subscribers/identify`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
app_user_id: userId,
|
||||||
|
new_app_user_id: newUserId,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
const parsed = IdentifyUserResponse.safeParse(json);
|
||||||
|
if (parsed.success) {
|
||||||
|
return parsed.data.was_created;
|
||||||
|
} else {
|
||||||
|
this.logger.error(
|
||||||
|
`RevenueCat identifyUser parse failed: ${JSON.stringify(
|
||||||
|
parsed.error.format()
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(`RevenueCat identifyUser failed: ${e.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getProducts(ent: Entitlement): Promise<Product[] | null> {
|
async getProducts(ent: Entitlement): Promise<Product[] | null> {
|
||||||
if (ent.products?.items && ent.products.items.length > 0) {
|
if (ent.products?.items && ent.products.items.length > 0) {
|
||||||
return ent.products.items;
|
return ent.products.items;
|
||||||
@@ -182,7 +221,10 @@ export class RevenueCatService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCustomerAlias(customerId: string): Promise<string[] | null> {
|
async getCustomerAlias(
|
||||||
|
customerId: string,
|
||||||
|
filterAlias = true
|
||||||
|
): Promise<string[] | null> {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://api.revenuecat.com/v2/projects/${this.projectId}/customers/${customerId}/aliases`,
|
`https://api.revenuecat.com/v2/projects/${this.projectId}/customers/${customerId}/aliases`,
|
||||||
{
|
{
|
||||||
@@ -204,9 +246,12 @@ export class RevenueCatService {
|
|||||||
const customerParsed = zRcV2RawCustomerAliasEnvelope.safeParse(json);
|
const customerParsed = zRcV2RawCustomerAliasEnvelope.safeParse(json);
|
||||||
|
|
||||||
if (customerParsed.success) {
|
if (customerParsed.success) {
|
||||||
return customerParsed.data.items
|
const customer = customerParsed.data.items.map(alias => alias.id);
|
||||||
.map(alias => alias.id)
|
if (filterAlias) {
|
||||||
.filter(id => !id.startsWith('$RCAnonymousID:'));
|
return customer.filter(id => !id.startsWith('$RCAnonymousID:'));
|
||||||
|
} else {
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`RevenueCat customer ${customerId} parse failed: ${JSON.stringify(
|
`RevenueCat customer ${customerId} parse failed: ${JSON.stringify(
|
||||||
|
|||||||
@@ -70,8 +70,14 @@ export class RevenueCatWebhookHandler {
|
|||||||
externalRef,
|
externalRef,
|
||||||
new Date(Date.now() + 10 * OneMinute) // expire after 10 minutes
|
new Date(Date.now() + 10 * OneMinute) // expire after 10 minutes
|
||||||
);
|
);
|
||||||
|
this.logger.log('Sync subscription by externalRef completed', {
|
||||||
|
appUserId,
|
||||||
|
externalRef,
|
||||||
|
subscriptions: subscriptions.map(s => s.identifier),
|
||||||
|
});
|
||||||
await this.queue.add('nightly.revenuecat.subscription.refresh', {
|
await this.queue.add('nightly.revenuecat.subscription.refresh', {
|
||||||
userId: appUserId,
|
userId: appUserId,
|
||||||
|
externalRef: externalRef,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,10 +317,9 @@ export class RevenueCatWebhookHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnJob('nightly.revenuecat.subscription.refresh.anonymous')
|
@OnJob('nightly.revenuecat.subscription.refresh.anonymous')
|
||||||
async onSubscriptionRefreshAnonymousUser(evt: {
|
async onSubscriptionRefreshAnonymousUser(
|
||||||
externalRef: string;
|
evt: Jobs['nightly.revenuecat.subscription.refresh.anonymous']
|
||||||
startTime: number;
|
) {
|
||||||
}) {
|
|
||||||
if (!this.config.payment.revenuecat?.enabled) return;
|
if (!this.config.payment.revenuecat?.enabled) return;
|
||||||
if (Date.now() - evt.startTime > REFRESH_MAX_TIMES) {
|
if (Date.now() - evt.startTime > REFRESH_MAX_TIMES) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
@@ -377,17 +382,47 @@ export class RevenueCatWebhookHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnJob('nightly.revenuecat.subscription.refresh')
|
@OnJob('nightly.revenuecat.subscription.refresh')
|
||||||
async onSubscriptionRefresh(evt: { userId: string; startTime: number }) {
|
async onSubscriptionRefresh(
|
||||||
|
evt: Jobs['nightly.revenuecat.subscription.refresh']
|
||||||
|
) {
|
||||||
if (!this.config.payment.revenuecat?.enabled) return;
|
if (!this.config.payment.revenuecat?.enabled) return;
|
||||||
if (Date.now() - evt.startTime > REFRESH_MAX_TIMES) {
|
const isTimeout = Date.now() - evt.startTime > REFRESH_MAX_TIMES;
|
||||||
this.logger.warn(
|
|
||||||
`RevenueCat subscription refresh timed out for user ${evt.userId}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
if (isTimeout) {
|
||||||
|
const subs = await this.rc.getSubscriptionByExternalRef(evt.externalRef);
|
||||||
|
const customers = Array.from(
|
||||||
|
new Set(
|
||||||
|
(subs?.map(sub => sub.customerId).filter(Boolean) as string[]) || []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const customerAliases = await Promise.all(
|
||||||
|
customers.map(custId =>
|
||||||
|
this.rc
|
||||||
|
.getCustomerAlias(custId, false)
|
||||||
|
.then(aliases =>
|
||||||
|
aliases?.length &&
|
||||||
|
aliases.filter(a => !a.startsWith('$RCAnonymousID:')).length === 0
|
||||||
|
? aliases[0]
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const oldUserId of customerAliases) {
|
||||||
|
if (oldUserId) {
|
||||||
|
await this.rc.identifyUser(oldUserId, evt.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const success = await this.syncAppUser(evt.userId);
|
const success = await this.syncAppUser(evt.userId);
|
||||||
if (success) return;
|
if (success) return;
|
||||||
|
if (isTimeout) {
|
||||||
|
this.logger.warn(`RevenueCat subscription refresh timed out`, {
|
||||||
|
userId: evt.userId,
|
||||||
|
externalRef: evt.externalRef,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
if (elapsed < REFRESH_INTERVAL) {
|
if (elapsed < REFRESH_INTERVAL) {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ declare global {
|
|||||||
interface Jobs {
|
interface Jobs {
|
||||||
'nightly.revenuecat.subscription.refresh': {
|
'nightly.revenuecat.subscription.refresh': {
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
|
externalRef: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
};
|
};
|
||||||
'nightly.revenuecat.subscription.refresh.anonymous': {
|
'nightly.revenuecat.subscription.refresh.anonymous': {
|
||||||
|
|||||||
Reference in New Issue
Block a user