Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/classic-shared/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export type BlocksToWebviewMessage =
payload: FeedbackResponse;
}
| {
type: 'PURCHASE_PRODUCT_SUCCESS_RESPONSE';
type: 'HARDCORE_ACCESS_UPDATE';
payload: {
access: HardcoreAccessStatus;
};
Expand Down
8 changes: 4 additions & 4 deletions packages/classic-webview/src/hooks/useHardcoreAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const hardcoreAccessContext = createContext<HardcoreAccessContext | null>(null);
export const HardcoreAccessContextProvider = (props: { children: React.ReactNode }) => {
const [access, setAccess] = useState<HardcoreAccessStatus>({ status: 'inactive' });
const hardcoreAccessInitResponse = useDevvitListener('HARDCORE_ACCESS_INIT_RESPONSE');
const productPurchaseResponse = useDevvitListener('PURCHASE_PRODUCT_SUCCESS_RESPONSE');
const hardcoreAccessUpdate = useDevvitListener('HARDCORE_ACCESS_UPDATE');

useEffect(() => {
if (hardcoreAccessInitResponse?.hardcoreAccessStatus != null) {
Expand All @@ -23,10 +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]);
}, [hardcoreAccessUpdate, setAccess]);

return (
<hardcoreAccessContext.Provider value={{ access, setAccess }}>
Expand Down
48 changes: 45 additions & 3 deletions packages/classic/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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';
Expand All @@ -21,6 +21,15 @@ 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';

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();

Expand Down Expand Up @@ -60,22 +69,55 @@ type InitialState =
hardcoreModeAccess: HardcoreAccessStatus;
};

const PURCHASE_REALTIME_CHANNEL = 'PURCHASE_REALTIME_CHANNEL';

// Add a post type definition
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) {
const msgCasted = msg as PurchasedProductBroadcast;
if (msgCasted.payload.userId === context.userId) {
sendMessageToWebview(context, {
type: 'HARDCORE_ACCESS_UPDATE',
payload: {
access: { status: 'active' },
},
});
}
},
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',
type: 'HARDCORE_ACCESS_UPDATE',
payload: {
access,
},
});
void purchaseRealtimeChannel.send({
payload: {
access: await paymentsRepo.getHardcoreAccessStatus(context.userId!),
userId: context.userId!,
},
});

break;
}
case OrderResultStatus.Error: {
Expand Down