Skip to content

Commit c9459eb

Browse files
authored
Broadcast successful purchase so all posts are refreshed with latest access state (#75)
1 parent b7838e6 commit c9459eb

File tree

3 files changed

+50
-8
lines changed

3 files changed

+50
-8
lines changed

packages/classic-shared/src/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export type BlocksToWebviewMessage =
173173
payload: FeedbackResponse;
174174
}
175175
| {
176-
type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE';
176+
type: 'HARDCORE_ACCESS_UPDATE';
177177
payload: {
178178
access: HardcoreAccessStatus;
179179
};

packages/classic-webview/src/hooks/useHardcoreAccess.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const hardcoreAccessContext = createContext<HardcoreAccessContext | null>(null);
1212
export const HardcoreAccessContextProvider = (props: { children: React.ReactNode }) => {
1313
const [access, setAccess] = useState<HardcoreAccessStatus>({ status: 'inactive' });
1414
const hardcoreAccessInitResponse = useDevvitListener('HARDCORE_ACCESS_INIT_RESPONSE');
15-
const productPurchaseResponse = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_RESPONSE');
15+
const hardcoreAccessUpdate = useDevvitListener('HARDCORE_ACCESS_UPDATE');
1616

1717
useEffect(() => {
1818
if (hardcoreAccessInitResponse?.hardcoreAccessStatus != null) {
@@ -23,10 +23,10 @@ export const HardcoreAccessContextProvider = (props: { children: React.ReactNode
2323
// When a purchase is successful, update 'access' state
2424
// `unlock hardcore` page and modal should react to this and act accordingly
2525
useEffect(() => {
26-
if (productPurchaseResponse != null) {
27-
setAccess(productPurchaseResponse.access);
26+
if (hardcoreAccessUpdate != null) {
27+
setAccess(hardcoreAccessUpdate.access);
2828
}
29-
}, [productPurchaseResponse, setAccess]);
29+
}, [hardcoreAccessUpdate, setAccess]);
3030

3131
return (
3232
<hardcoreAccessContext.Provider value={{ access, setAccess }}>

packages/classic/src/main.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import './menu-actions/newChallenge.js';
66
import './menu-actions/addWordToDictionary.js';
77
import './menu-actions/totalReminders.js';
88

9-
import { Devvit, useInterval, useState } from '@devvit/public-api';
9+
import { Devvit, JSONValue, useInterval, useState } from '@devvit/public-api';
1010
import { DEVVIT_SETTINGS_KEYS } from './constants.js';
1111
import { isServerCall, omit } from '@hotandcold/shared/utils';
1212
import { GameMode, HardcoreAccessStatus, WebviewToBlocksMessage } from '@hotandcold/classic-shared';
@@ -21,6 +21,15 @@ import { RedditApiCache } from './core/redditApiCache.js';
2121
import { sendMessageToWebview } from './utils/index.js';
2222
import { initPayments, PaymentsRepo } from './payments.js';
2323
import { OnPurchaseResult, OrderResultStatus, usePayments } from '@devvit/payments';
24+
import { useChannel } from '@devvit/public-api';
25+
26+
export type PurchasedProductBroadcast = {
27+
payload: {
28+
// user who purchased the product; important because we don't want the broadcast to unlock
29+
// hardcore for all users
30+
userId: string;
31+
};
32+
};
2433

2534
initPayments();
2635

@@ -60,22 +69,55 @@ type InitialState =
6069
hardcoreModeAccess: HardcoreAccessStatus;
6170
};
6271

72+
const PURCHASE_REALTIME_CHANNEL = 'PURCHASE_REALTIME_CHANNEL';
73+
6374
// Add a post type definition
6475
Devvit.addCustomPostType({
6576
name: 'HotAndCold',
6677
height: 'tall',
6778
render: (context) => {
79+
// This channel is used to broadcast purchase success events to all instances of the app.
80+
// It's necessary because iOS and Android aggressively cache webviews, which can cause
81+
// the purchase success state to not be reflected immediately in all open instances.
82+
// By broadcasting the event through a realtime channel, we ensure all instances
83+
// update their UI state correctly, even if they're cached.
84+
const purchaseRealtimeChannel = useChannel({
85+
name: PURCHASE_REALTIME_CHANNEL,
86+
onMessage(msg: JSONValue) {
87+
const msgCasted = msg as PurchasedProductBroadcast;
88+
if (msgCasted.payload.userId === context.userId) {
89+
sendMessageToWebview(context, {
90+
type: 'HARDCORE_ACCESS_UPDATE',
91+
payload: {
92+
access: { status: 'active' },
93+
},
94+
});
95+
}
96+
},
97+
onSubscribed: () => {
98+
console.log('listening for purchase success broadcast events');
99+
},
100+
});
101+
purchaseRealtimeChannel.subscribe();
102+
68103
const paymentsRepo = new PaymentsRepo(context.redis);
69104
const payments = usePayments(async (paymentsResult: OnPurchaseResult) => {
70105
switch (paymentsResult.status) {
71106
case OrderResultStatus.Success: {
72107
context.ui.showToast(`Purchase successful!`);
108+
const access = await paymentsRepo.getHardcoreAccessStatus(context.userId!);
73109
sendMessageToWebview(context, {
74-
type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE',
110+
type: 'HARDCORE_ACCESS_UPDATE',
111+
payload: {
112+
access,
113+
},
114+
});
115+
void purchaseRealtimeChannel.send({
75116
payload: {
76-
access: await paymentsRepo.getHardcoreAccessStatus(context.userId!),
117+
userId: context.userId!,
77118
},
78119
});
120+
79121
break;
80122
}
81123
case OrderResultStatus.Error: {

0 commit comments

Comments
 (0)