diff --git a/package-lock.json b/package-lock.json
index 0ec4c324..6a655ac9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11133,7 +11133,8 @@
"version": "0.0.0",
"license": "BSD-3-Clause",
"dependencies": {
- "@devvit/public-api": "^0.11.13",
+ "@devvit/payments": "0.11.12",
+ "@devvit/public-api": "0.11.12",
"@hotandcold/classic-shared": "*",
"@hotandcold/shared": "*",
"luxon": "^3.6.1"
@@ -11194,6 +11195,69 @@
"node": ">=14.17"
}
},
+ "packages/classic/node_modules/@devvit/metrics": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/@devvit/metrics/-/metrics-0.11.12.tgz",
+ "integrity": "sha512-vQ12EK+1fmf0ivseLgWsjW5o1TDT/L1oCN/XJotV6ca85bToqu3VQFtNOjzRkC/9CUeHbRRFZXwfPpKf5m2EJQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@devvit/protos": "0.11.12"
+ }
+ },
+ "packages/classic/node_modules/@devvit/payments": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/@devvit/payments/-/payments-0.11.12.tgz",
+ "integrity": "sha512-AsBkynEn0pmHv9a8tCB9e2mBcrqjtujOJMcLMe0bGHNerjPtN11f9XVnN4t8lBi9ArLTKMmQo7HEzJSBTa4U5Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@devvit/protos": "0.11.12",
+ "@devvit/public-api": "0.11.12",
+ "@devvit/shared-types": "0.11.12"
+ }
+ },
+ "packages/classic/node_modules/@devvit/protos": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/@devvit/protos/-/protos-0.11.12.tgz",
+ "integrity": "sha512-SMPKHhhsdAT8sv8Ps/Z9stWQU/Sl365CMoiG3eSxRUKJ7tGGAqCDDwGJgaLZB5U7wGPg6Zkt9+JB70RFdypyvQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "protobufjs": "7.3.2",
+ "rxjs": "7.8.1"
+ },
+ "peerDependencies": {
+ "twirp-ts": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "twirp-ts": {
+ "optional": true
+ }
+ }
+ },
+ "packages/classic/node_modules/@devvit/public-api": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/@devvit/public-api/-/public-api-0.11.12.tgz",
+ "integrity": "sha512-kIYLp7mDHHBqneBfeGtTieW9cY+32WQpz++cEJjViQOhtSzmd0yFhfWdILfjPnDD/+vTKHZJChF0CdodGgt48A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@devvit/metrics": "0.11.12",
+ "@devvit/protos": "0.11.12",
+ "@devvit/shared-types": "0.11.12",
+ "base64-js": "1.5.1",
+ "clone-deep": "4.0.1",
+ "moderndash": "4.0.0"
+ }
+ },
+ "packages/classic/node_modules/@devvit/shared-types": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/@devvit/shared-types/-/shared-types-0.11.12.tgz",
+ "integrity": "sha512-ZbMCijsckqg+PIcHffKd4AKMAHXS50ht2CdgXzmuSLh7PR02LySP94Wq7PpT4sNMAV/d5rNt/pax8/PHuobvkg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@devvit/protos": "0.11.12",
+ "jsonschema": "1.4.1",
+ "uuid": "9.0.0"
+ }
+ },
"packages/raid": {
"name": "@hotandcold/raid",
"version": "0.0.0",
diff --git a/packages/classic-shared/src/shared.ts b/packages/classic-shared/src/shared.ts
index fa6321ae..8c88d5dd 100644
--- a/packages/classic-shared/src/shared.ts
+++ b/packages/classic-shared/src/shared.ts
@@ -1,4 +1,4 @@
-export type Page = 'loading' | 'play' | 'stats' | 'win';
+export type Page = 'loading' | 'play' | 'stats' | 'win' | 'unlock-hardcore';
export type Guess = {
word: string;
@@ -59,6 +59,7 @@ export type Game = {
username: string;
};
challengeProgress: PlayerProgress;
+ hardcoreModeAccess: HardcoreAccessStatus;
};
export type GameResponse = Game;
@@ -80,6 +81,11 @@ export type ChallengeLeaderboardResponse = {
leaderboardByFastest: { member: string; score: number }[];
};
+export type HardcoreAccessStatus =
+ | { status: 'lifetime' }
+ | { status: 'active'; expires: number }
+ | { status: 'inactive' };
+
export type WebviewToBlocksMessage =
| { type: 'GAME_INIT' }
| {
diff --git a/packages/classic-webview/src/App.tsx b/packages/classic-webview/src/App.tsx
index ba316a05..70f7d0d1 100644
--- a/packages/classic-webview/src/App.tsx
+++ b/packages/classic-webview/src/App.tsx
@@ -9,6 +9,11 @@ import { cn } from '@hotandcold/webview-common/utils';
import { Header } from './components/header';
import { LoadingPage } from './pages/LoadingPage';
+import { useModal } from './hooks/useModal';
+import { UnlockHardcoreModal } from './components/UnlockHardcoreModal';
+import { HowToPlayModal } from './components/howToPlayModal';
+import { ScoreBreakdownModal } from './components/scoreBreakdownModal';
+
const getPage = (page: Page) => {
switch (page) {
case 'play':
@@ -19,6 +24,9 @@ const getPage = (page: Page) => {
return ;
case 'loading':
return ;
+ case 'unlock-hardcore':
+ // TODO: Implement page for hardcore mode
+ return
UNLOCK HARDCORD
;
default:
throw new Error(`Invalid page: ${String(page satisfies never)}`);
}
@@ -27,6 +35,18 @@ const getPage = (page: Page) => {
export const App = () => {
const page = usePage();
const { mode } = useGame();
+ const { modal, closeModal } = useModal();
+
+ if (modal != null) {
+ switch (modal) {
+ case 'unlock-hardcore':
+ return ;
+ case 'how-to-play':
+ return ;
+ case 'score-breakdown':
+ return ;
+ }
+ }
return (
{
{getPage(page)}
- {/* setFriendsModalOpen(false)} /> */}
);
};
diff --git a/packages/classic-webview/src/components/UnlockHardcoreModal.tsx b/packages/classic-webview/src/components/UnlockHardcoreModal.tsx
new file mode 100644
index 00000000..5d8157f1
--- /dev/null
+++ b/packages/classic-webview/src/components/UnlockHardcoreModal.tsx
@@ -0,0 +1,64 @@
+import React, { ComponentProps } from 'react';
+import { HardcoreLogo } from '@hotandcold/webview-common/components/logo';
+import { GoldIcon } from '@hotandcold/webview-common/components/icon';
+import { cn } from '@hotandcold/webview-common/utils';
+import { Modal } from '@hotandcold/webview-common/components/modal';
+
+interface PurchaseButtonProps {
+ children: React.ReactNode;
+ style: 'primary' | 'secondary';
+ price: number;
+ onClick?: () => void;
+}
+
+const PurchaseButton: React.FC = (props) => {
+ const { children, price, onClick, style } = props;
+
+ return (
+
+ );
+};
+
+type UnlockHardcoreModalProps = Omit, 'children'>;
+
+export const UnlockHardcoreModal = (props: UnlockHardcoreModalProps) => {
+ return (
+
+
+
+
100 guesses. No hints. No mercy.
+
+ Unlocking Hardcore grants access to today and all previous hardcore puzzles.
+
+
+
+
+ Unlock for 7 days
+
+
+ Unlock FOREVER
+
+
+
+
+ );
+};
diff --git a/packages/classic-webview/src/components/header.tsx b/packages/classic-webview/src/components/header.tsx
index bc331659..8d3b4c01 100644
--- a/packages/classic-webview/src/components/header.tsx
+++ b/packages/classic-webview/src/components/header.tsx
@@ -4,11 +4,10 @@ import { useConfirmation } from '@hotandcold/webview-common/hooks/useConfirmatio
import { sendMessageToDevvit } from '../utils';
import { useUserSettings, useSetUserSettings } from '../hooks/useUserSettings';
import { useGame } from '../hooks/useGame';
-import { useState } from 'react';
import type { UserSettings } from '@hotandcold/classic-shared';
-import { HowToPlayModal } from './howToPlayModal';
import { IconButton } from '@hotandcold/webview-common/components/button';
import { InfoIcon } from '@hotandcold/webview-common/components/icon';
+import { useModal } from '../hooks/useModal';
const SpeechBubbleTail = ({ className }: { className?: string }) => (
);
+export const GoldIcon = () => (
+
+);
+
export const RightChevronIcon = () => (