feat(chat): add promotional external entry flow
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
consumePendingChatPromotion,
|
||||
peekPendingChatUnlock,
|
||||
} from "@/lib/navigation/chat_unlock_session";
|
||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
const log = new Logger("UseChatPromotionBootstrap");
|
||||
|
||||
export function useChatPromotionBootstrap(): boolean {
|
||||
const chatDispatch = useChatDispatch();
|
||||
const startedRef = useRef(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (startedRef.current) return;
|
||||
startedRef.current = true;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [entryPromotion, pendingUnlock] = await Promise.all([
|
||||
consumePendingChatPromotion(),
|
||||
peekPendingChatUnlock(),
|
||||
]);
|
||||
const promotion = entryPromotion ?? pendingUnlock?.promotion ?? null;
|
||||
|
||||
if (promotion) {
|
||||
chatDispatch({
|
||||
type: "ChatPromotionInjected",
|
||||
promotion,
|
||||
messageId: pendingUnlock?.promotion
|
||||
? pendingUnlock.messageId
|
||||
: undefined,
|
||||
});
|
||||
} else {
|
||||
chatDispatch({ type: "ChatPromotionCleared" });
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Failed to restore chat promotion", error);
|
||||
chatDispatch({ type: "ChatPromotionCleared" });
|
||||
} finally {
|
||||
setIsReady(true);
|
||||
}
|
||||
})();
|
||||
}, [chatDispatch]);
|
||||
|
||||
return isReady;
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import type { ChatLockType } from "@/data/schemas/chat";
|
||||
import {
|
||||
consumePendingChatUnlock,
|
||||
peekPendingChatUnlock,
|
||||
type PendingChatUnlock,
|
||||
type PendingChatUnlockKind,
|
||||
type PendingChatPromotion,
|
||||
} from "@/lib/navigation/chat_unlock_session";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
||||
@@ -15,11 +17,21 @@ import { useUserState } from "@/stores/user/user-context";
|
||||
|
||||
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
|
||||
|
||||
type PromotionScope = "any" | "only" | "exclude";
|
||||
|
||||
export interface MessageUnlockOptions {
|
||||
remoteMessageId?: string;
|
||||
lockType?: ChatLockType;
|
||||
clientLockId?: string;
|
||||
promotion?: PendingChatPromotion;
|
||||
}
|
||||
|
||||
export interface UseChatUnlockNavigationFlowInput {
|
||||
returnUrl: string;
|
||||
expectedKind?: PendingChatUnlockKind;
|
||||
expectedMessageId?: string;
|
||||
ignoredKind?: PendingChatUnlockKind;
|
||||
promotionScope?: PromotionScope;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -28,6 +40,7 @@ export interface UseChatUnlockNavigationFlowOutput {
|
||||
requestMessageUnlock: (
|
||||
messageId: string,
|
||||
kind: PendingChatUnlockKind,
|
||||
options?: MessageUnlockOptions,
|
||||
) => void;
|
||||
closeInsufficientCreditsDialog: () => void;
|
||||
confirmInsufficientCreditsDialog: () => void;
|
||||
@@ -38,6 +51,7 @@ export function useChatUnlockNavigationFlow({
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
ignoredKind,
|
||||
promotionScope = "any",
|
||||
enabled = true,
|
||||
}: UseChatUnlockNavigationFlowInput): UseChatUnlockNavigationFlowOutput {
|
||||
const navigator = useAppNavigator();
|
||||
@@ -49,6 +63,7 @@ export function useChatUnlockNavigationFlow({
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
ignoredKind,
|
||||
promotionScope,
|
||||
enabled,
|
||||
});
|
||||
|
||||
@@ -68,6 +83,7 @@ export function useChatUnlockNavigationFlow({
|
||||
pending,
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
promotionScope,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
@@ -78,8 +94,11 @@ export function useChatUnlockNavigationFlow({
|
||||
|
||||
chatDispatch({
|
||||
type: "ChatUnlockMessageRequested",
|
||||
messageId: consumed.messageId,
|
||||
messageId: consumed.displayMessageId,
|
||||
remoteMessageId: consumed.messageId,
|
||||
kind: consumed.kind,
|
||||
lockType: consumed.lockType,
|
||||
clientLockId: consumed.clientLockId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -93,24 +112,34 @@ export function useChatUnlockNavigationFlow({
|
||||
enabled,
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
ignoredKind,
|
||||
navigator.isAuthenticatedUser,
|
||||
promotionScope,
|
||||
returnUrl,
|
||||
]);
|
||||
|
||||
function requestMessageUnlock(
|
||||
messageId: string,
|
||||
kind: PendingChatUnlockKind,
|
||||
options: MessageUnlockOptions = {},
|
||||
): void {
|
||||
const remoteMessageId =
|
||||
options.remoteMessageId ?? (options.lockType ? undefined : messageId);
|
||||
navigator.startMessageUnlock({
|
||||
messageId,
|
||||
displayMessageId: messageId,
|
||||
messageId: remoteMessageId,
|
||||
kind,
|
||||
lockType: options.lockType,
|
||||
clientLockId: options.clientLockId,
|
||||
promotion: options.promotion,
|
||||
returnUrl,
|
||||
onAuthenticated: () => {
|
||||
chatDispatch({
|
||||
type: "ChatUnlockMessageRequested",
|
||||
messageId,
|
||||
remoteMessageId,
|
||||
kind,
|
||||
lockType: options.lockType,
|
||||
clientLockId: options.clientLockId,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -125,8 +154,12 @@ export function useChatUnlockNavigationFlow({
|
||||
|
||||
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
|
||||
navigator.openSubscriptionForPendingUnlock({
|
||||
displayMessageId: unlockPaywallRequest.displayMessageId,
|
||||
messageId: unlockPaywallRequest.messageId,
|
||||
kind: unlockPaywallRequest.kind,
|
||||
lockType: unlockPaywallRequest.lockType,
|
||||
clientLockId: unlockPaywallRequest.clientLockId,
|
||||
promotion: unlockPaywallRequest.promotion,
|
||||
returnUrl,
|
||||
type: getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||
});
|
||||
@@ -145,6 +178,7 @@ function getScopedUnlockPaywallRequest(input: {
|
||||
expectedKind?: PendingChatUnlockKind;
|
||||
expectedMessageId?: string;
|
||||
ignoredKind?: PendingChatUnlockKind;
|
||||
promotionScope?: PromotionScope;
|
||||
enabled?: boolean;
|
||||
}): ChatUnlockPaywallRequest | null {
|
||||
const {
|
||||
@@ -152,13 +186,21 @@ function getScopedUnlockPaywallRequest(input: {
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
ignoredKind,
|
||||
promotionScope = "any",
|
||||
enabled = true,
|
||||
} = input;
|
||||
if (!enabled) return null;
|
||||
if (!request) return null;
|
||||
if (!enabled || !request) return null;
|
||||
if (promotionScope === "only" && !request.promotion) return null;
|
||||
if (promotionScope === "exclude" && request.promotion) return null;
|
||||
if (ignoredKind && request.kind === ignoredKind) return null;
|
||||
if (expectedKind && request.kind !== expectedKind) return null;
|
||||
if (expectedMessageId && request.messageId !== expectedMessageId) return null;
|
||||
if (
|
||||
expectedMessageId &&
|
||||
request.displayMessageId !== expectedMessageId &&
|
||||
request.messageId !== expectedMessageId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -166,10 +208,22 @@ function matchesPendingUnlockScope(input: {
|
||||
pending: PendingChatUnlock;
|
||||
expectedKind?: PendingChatUnlockKind;
|
||||
expectedMessageId?: string;
|
||||
promotionScope?: PromotionScope;
|
||||
}): boolean {
|
||||
const { pending, expectedKind, expectedMessageId } = input;
|
||||
const {
|
||||
pending,
|
||||
expectedKind,
|
||||
expectedMessageId,
|
||||
promotionScope = "any",
|
||||
} = input;
|
||||
if (promotionScope === "only" && !pending.promotion) return false;
|
||||
if (promotionScope === "exclude" && pending.promotion) return false;
|
||||
if (expectedKind && pending.kind !== expectedKind) return false;
|
||||
if (expectedMessageId && pending.messageId !== expectedMessageId) {
|
||||
if (
|
||||
expectedMessageId &&
|
||||
pending.displayMessageId !== expectedMessageId &&
|
||||
pending.messageId !== expectedMessageId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user