From 0ab6c9d24b4b36a719a5ba9c0ee868616465ebf4 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Tue, 6 May 2025 14:42:04 -0400 Subject: [PATCH 1/5] noice --- packages/classic-shared/src/shared.ts | 13 +++++++ .../src/hooks/useHardcoreAccess.tsx | 7 ++++ packages/classic/src/main.tsx | 36 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts index 9865826f..68fbeaf7 100644 --- a/packages/classic-shared/src/shared.ts +++ b/packages/classic-shared/src/shared.ts @@ -177,6 +177,13 @@ export type BlocksToWebviewMessage = payload: { access: HardcoreAccessStatus; }; + } + | { + // emitted when a purchase is successfully made on another post + type: 'PURCHASE_PRODUCT_SUCCESS_BROADCAST'; + payload: { + access: HardcoreAccessStatus; + }; }; export type DevvitMessage = { @@ -185,3 +192,9 @@ export type DevvitMessage = { }; export type GameMode = 'regular' | 'hardcore'; + +export type PurchasedProductBroadcast = { + payload: { + access: HardcoreAccessStatus; + }; +}; diff --git a/packages/classic-webview/src/hooks/useHardcoreAccess.tsx b/packages/classic-webview/src/hooks/useHardcoreAccess.tsx index 5de1ff6b..f97fd1f6 100644 --- a/packages/classic-webview/src/hooks/useHardcoreAccess.tsx +++ b/packages/classic-webview/src/hooks/useHardcoreAccess.tsx @@ -13,6 +13,7 @@ export const HardcoreAccessContextProvider = (props: { children: React.ReactNode const [access, setAccess] = useState({ status: 'inactive' }); const hardcoreAccessInitResponse = useDevvitListener('HARDCORE_ACCESS_INIT_RESPONSE'); const productPurchaseResponse = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_RESPONSE'); + const productPurchaseBroadcast = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_BROADCAST'); useEffect(() => { if (hardcoreAccessInitResponse?.hardcoreAccessStatus != null) { @@ -28,6 +29,12 @@ export const HardcoreAccessContextProvider = (props: { children: React.ReactNode } }, [productPurchaseResponse, setAccess]); + useEffect(() => { + if (productPurchaseBroadcast != null) { + setAccess(productPurchaseBroadcast.access); + } + }, [productPurchaseBroadcast, setAccess]); + return ( {props.children} diff --git a/packages/classic/src/main.tsx b/packages/classic/src/main.tsx index d66c08ee..1991655e 100644 --- a/packages/classic/src/main.tsx +++ b/packages/classic/src/main.tsx @@ -6,10 +6,15 @@ import './menu-actions/newChallenge.js'; import './menu-actions/addWordToDictionary.js'; import './menu-actions/totalReminders.js'; -import { Devvit, useInterval, useState } from '@devvit/public-api'; +import { Devvit, JSONValue, useInterval, useState } from '@devvit/public-api'; import { DEVVIT_SETTINGS_KEYS } from './constants.js'; import { isServerCall, omit } from '@hotandcold/shared/utils'; -import { GameMode, HardcoreAccessStatus, WebviewToBlocksMessage } from '@hotandcold/classic-shared'; +import { + GameMode, + HardcoreAccessStatus, + PurchasedProductBroadcast as PurchasedProductBroadcastMessage, + WebviewToBlocksMessage, +} from '@hotandcold/classic-shared'; import { GuessService } from './core/guess.js'; import { ChallengeToPost, PostIdentifier } from './core/challengeToPost.js'; import { Preview } from './components/Preview.js'; @@ -21,6 +26,7 @@ import { RedditApiCache } from './core/redditApiCache.js'; import { sendMessageToWebview } from './utils/index.js'; import { initPayments, PaymentsRepo } from './payments.js'; import { OnPurchaseResult, OrderResultStatus, usePayments } from '@devvit/payments'; +import { useChannel } from '@devvit/public-api'; initPayments(); @@ -60,22 +66,46 @@ type InitialState = hardcoreModeAccess: HardcoreAccessStatus; }; +const PURCHASE_REALTIME_CHANNEL = 'PURCHASE_REALTIME_CHANNEL'; + // Add a post type definition Devvit.addCustomPostType({ name: 'HotAndCold', height: 'tall', render: (context) => { + const purchaseRealtimeChannel = useChannel({ + name: PURCHASE_REALTIME_CHANNEL, + onMessage(msg: JSONValue) { + const msgCasted = msg as PurchasedProductBroadcastMessage; + sendMessageToWebview(context, { + type: 'PURCHASE_PRODUCT_SUCCESS_BROADCAST', + payload: msgCasted.payload, + }); + }, + onSubscribed: () => { + console.log('listening for purchase success broadcast events'); + }, + }); + purchaseRealtimeChannel.subscribe(); + const paymentsRepo = new PaymentsRepo(context.redis); const payments = usePayments(async (paymentsResult: OnPurchaseResult) => { switch (paymentsResult.status) { case OrderResultStatus.Success: { context.ui.showToast(`Purchase successful!`); + const access = await paymentsRepo.getHardcoreAccessStatus(context.userId!); sendMessageToWebview(context, { type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE', payload: { - access: await paymentsRepo.getHardcoreAccessStatus(context.userId!), + access, }, }); + await purchaseRealtimeChannel.send({ + payload: { + access, + }, + }); + break; } case OrderResultStatus.Error: { From 62fa6442e3abf39c30e16022be8890753c78114f Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Tue, 6 May 2025 14:44:22 -0400 Subject: [PATCH 2/5] update comment --- packages/classic-shared/src/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts index 68fbeaf7..23197d59 100644 --- a/packages/classic-shared/src/shared.ts +++ b/packages/classic-shared/src/shared.ts @@ -179,7 +179,7 @@ export type BlocksToWebviewMessage = }; } | { - // emitted when a purchase is successfully made on another post + // relayed broadcast message from PURCHASE_REALTIME_CHANNEL channel type: 'PURCHASE_PRODUCT_SUCCESS_BROADCAST'; payload: { access: HardcoreAccessStatus; From 7e224edce3182c0ad7f513c5f813f1c8e10b5d2d Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Tue, 6 May 2025 17:03:14 -0400 Subject: [PATCH 3/5] pr feedback --- packages/classic-shared/src/shared.ts | 9 +-------- .../src/hooks/useHardcoreAccess.tsx | 15 ++++----------- packages/classic/src/main.tsx | 6 +++--- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts index 23197d59..48db426f 100644 --- a/packages/classic-shared/src/shared.ts +++ b/packages/classic-shared/src/shared.ts @@ -173,14 +173,7 @@ export type BlocksToWebviewMessage = payload: FeedbackResponse; } | { - type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE'; - payload: { - access: HardcoreAccessStatus; - }; - } - | { - // relayed broadcast message from PURCHASE_REALTIME_CHANNEL channel - type: 'PURCHASE_PRODUCT_SUCCESS_BROADCAST'; + type: 'HARDCORE_ACCESS_UPDATE'; payload: { access: HardcoreAccessStatus; }; diff --git a/packages/classic-webview/src/hooks/useHardcoreAccess.tsx b/packages/classic-webview/src/hooks/useHardcoreAccess.tsx index f97fd1f6..aaf4bf6f 100644 --- a/packages/classic-webview/src/hooks/useHardcoreAccess.tsx +++ b/packages/classic-webview/src/hooks/useHardcoreAccess.tsx @@ -12,8 +12,7 @@ const hardcoreAccessContext = createContext(null); export const HardcoreAccessContextProvider = (props: { children: React.ReactNode }) => { const [access, setAccess] = useState({ status: 'inactive' }); const hardcoreAccessInitResponse = useDevvitListener('HARDCORE_ACCESS_INIT_RESPONSE'); - const productPurchaseResponse = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_RESPONSE'); - const productPurchaseBroadcast = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_BROADCAST'); + const hardcoreAccessUpdate = useDevvitListener('HARDCORE_ACCESS_UPDATE'); useEffect(() => { if (hardcoreAccessInitResponse?.hardcoreAccessStatus != null) { @@ -24,16 +23,10 @@ export const HardcoreAccessContextProvider = (props: { children: React.ReactNode // When a purchase is successful, update 'access' state // `unlock hardcore` page and modal should react to this and act accordingly useEffect(() => { - if (productPurchaseResponse != null) { - setAccess(productPurchaseResponse.access); + if (hardcoreAccessUpdate != null) { + setAccess(hardcoreAccessUpdate.access); } - }, [productPurchaseResponse, setAccess]); - - useEffect(() => { - if (productPurchaseBroadcast != null) { - setAccess(productPurchaseBroadcast.access); - } - }, [productPurchaseBroadcast, setAccess]); + }, [hardcoreAccessUpdate, setAccess]); return ( diff --git a/packages/classic/src/main.tsx b/packages/classic/src/main.tsx index 1991655e..e07f0ac2 100644 --- a/packages/classic/src/main.tsx +++ b/packages/classic/src/main.tsx @@ -78,7 +78,7 @@ Devvit.addCustomPostType({ onMessage(msg: JSONValue) { const msgCasted = msg as PurchasedProductBroadcastMessage; sendMessageToWebview(context, { - type: 'PURCHASE_PRODUCT_SUCCESS_BROADCAST', + type: 'HARDCORE_ACCESS_UPDATE', payload: msgCasted.payload, }); }, @@ -95,12 +95,12 @@ Devvit.addCustomPostType({ context.ui.showToast(`Purchase successful!`); const access = await paymentsRepo.getHardcoreAccessStatus(context.userId!); sendMessageToWebview(context, { - type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE', + type: 'HARDCORE_ACCESS_UPDATE', payload: { access, }, }); - await purchaseRealtimeChannel.send({ + void purchaseRealtimeChannel.send({ payload: { access, }, From 4f8a35da9223dda0189e80750ee366239f168398 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Wed, 7 May 2025 11:21:37 -0400 Subject: [PATCH 4/5] pr feedback --- packages/classic-shared/src/shared.ts | 3 +++ packages/classic/src/main.tsx | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts index 48db426f..02f36a55 100644 --- a/packages/classic-shared/src/shared.ts +++ b/packages/classic-shared/src/shared.ts @@ -189,5 +189,8 @@ export type GameMode = 'regular' | 'hardcore'; export type PurchasedProductBroadcast = { payload: { access: HardcoreAccessStatus; + // user who purchased the product; important because we don't want the broadcast to unlock + // hardcore for all users + userId: string; }; }; diff --git a/packages/classic/src/main.tsx b/packages/classic/src/main.tsx index e07f0ac2..ec491ab7 100644 --- a/packages/classic/src/main.tsx +++ b/packages/classic/src/main.tsx @@ -12,7 +12,7 @@ import { isServerCall, omit } from '@hotandcold/shared/utils'; import { GameMode, HardcoreAccessStatus, - PurchasedProductBroadcast as PurchasedProductBroadcastMessage, + PurchasedProductBroadcast, WebviewToBlocksMessage, } from '@hotandcold/classic-shared'; import { GuessService } from './core/guess.js'; @@ -76,11 +76,15 @@ Devvit.addCustomPostType({ const purchaseRealtimeChannel = useChannel({ name: PURCHASE_REALTIME_CHANNEL, onMessage(msg: JSONValue) { - const msgCasted = msg as PurchasedProductBroadcastMessage; - sendMessageToWebview(context, { - type: 'HARDCORE_ACCESS_UPDATE', - payload: msgCasted.payload, - }); + const msgCasted = msg as PurchasedProductBroadcast; + if (msgCasted.payload.userId === context.userId) { + sendMessageToWebview(context, { + type: 'HARDCORE_ACCESS_UPDATE', + payload: { + access: msgCasted.payload.access, + }, + }); + } }, onSubscribed: () => { console.log('listening for purchase success broadcast events'); @@ -103,6 +107,7 @@ Devvit.addCustomPostType({ void purchaseRealtimeChannel.send({ payload: { access, + userId: context.userId!, }, }); From 62869d0fd7072a318f0216c418d94f82a261f703 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Wed, 7 May 2025 14:41:59 -0400 Subject: [PATCH 5/5] pr feedback --- packages/classic-shared/src/shared.ts | 9 --------- packages/classic/src/main.tsx | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts index 02f36a55..368192dd 100644 --- a/packages/classic-shared/src/shared.ts +++ b/packages/classic-shared/src/shared.ts @@ -185,12 +185,3 @@ export type DevvitMessage = { }; export type GameMode = 'regular' | 'hardcore'; - -export type PurchasedProductBroadcast = { - payload: { - access: HardcoreAccessStatus; - // user who purchased the product; important because we don't want the broadcast to unlock - // hardcore for all users - userId: string; - }; -}; diff --git a/packages/classic/src/main.tsx b/packages/classic/src/main.tsx index ec491ab7..f0f065d8 100644 --- a/packages/classic/src/main.tsx +++ b/packages/classic/src/main.tsx @@ -9,12 +9,7 @@ import './menu-actions/totalReminders.js'; import { Devvit, JSONValue, useInterval, useState } from '@devvit/public-api'; import { DEVVIT_SETTINGS_KEYS } from './constants.js'; import { isServerCall, omit } from '@hotandcold/shared/utils'; -import { - GameMode, - HardcoreAccessStatus, - PurchasedProductBroadcast, - WebviewToBlocksMessage, -} from '@hotandcold/classic-shared'; +import { GameMode, HardcoreAccessStatus, WebviewToBlocksMessage } from '@hotandcold/classic-shared'; import { GuessService } from './core/guess.js'; import { ChallengeToPost, PostIdentifier } from './core/challengeToPost.js'; import { Preview } from './components/Preview.js'; @@ -28,6 +23,14 @@ import { initPayments, PaymentsRepo } from './payments.js'; import { OnPurchaseResult, OrderResultStatus, usePayments } from '@devvit/payments'; import { useChannel } from '@devvit/public-api'; +export type PurchasedProductBroadcast = { + payload: { + // user who purchased the product; important because we don't want the broadcast to unlock + // hardcore for all users + userId: string; + }; +}; + initPayments(); Devvit.configure({ @@ -73,6 +76,11 @@ Devvit.addCustomPostType({ name: 'HotAndCold', height: 'tall', render: (context) => { + // This channel is used to broadcast purchase success events to all instances of the app. + // It's necessary because iOS and Android aggressively cache webviews, which can cause + // the purchase success state to not be reflected immediately in all open instances. + // By broadcasting the event through a realtime channel, we ensure all instances + // update their UI state correctly, even if they're cached. const purchaseRealtimeChannel = useChannel({ name: PURCHASE_REALTIME_CHANNEL, onMessage(msg: JSONValue) { @@ -81,7 +89,7 @@ Devvit.addCustomPostType({ sendMessageToWebview(context, { type: 'HARDCORE_ACCESS_UPDATE', payload: { - access: msgCasted.payload.access, + access: { status: 'active' }, }, }); } @@ -106,7 +114,6 @@ Devvit.addCustomPostType({ }); void purchaseRealtimeChannel.send({ payload: { - access, userId: context.userId!, }, });