@sudobility/sudojo_lib 0.0.200 → 0.0.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/adminApi.d.ts +34 -0
- package/dist/admin/adminApi.d.ts.map +1 -0
- package/dist/admin/adminApi.js +59 -0
- package/dist/admin/adminApi.js.map +1 -0
- package/dist/admin/boardRules.d.ts +75 -0
- package/dist/admin/boardRules.d.ts.map +1 -0
- package/dist/admin/boardRules.js +127 -0
- package/dist/admin/boardRules.js.map +1 -0
- package/dist/admin/boardStrings.d.ts +55 -0
- package/dist/admin/boardStrings.d.ts.map +1 -0
- package/dist/admin/boardStrings.js +99 -0
- package/dist/admin/boardStrings.js.map +1 -0
- package/dist/admin/examples.d.ts +58 -0
- package/dist/admin/examples.d.ts.map +1 -0
- package/dist/admin/examples.js +104 -0
- package/dist/admin/examples.js.map +1 -0
- package/dist/admin/extraction.d.ts +87 -0
- package/dist/admin/extraction.d.ts.map +1 -0
- package/dist/admin/extraction.js +141 -0
- package/dist/admin/extraction.js.map +1 -0
- package/dist/admin/index.d.ts +32 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +22 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/jobs.d.ts +108 -0
- package/dist/admin/jobs.d.ts.map +1 -0
- package/dist/admin/jobs.js +435 -0
- package/dist/admin/jobs.js.map +1 -0
- package/dist/admin/regenerateHints.d.ts +16 -0
- package/dist/admin/regenerateHints.d.ts.map +1 -0
- package/dist/admin/regenerateHints.js +43 -0
- package/dist/admin/regenerateHints.js.map +1 -0
- package/dist/admin/types.d.ts +42 -0
- package/dist/admin/types.d.ts.map +1 -0
- package/dist/admin/types.js +21 -0
- package/dist/admin/types.js.map +1 -0
- package/dist/admin/useAdminJob.d.ts +28 -0
- package/dist/admin/useAdminJob.d.ts.map +1 -0
- package/dist/admin/useAdminJob.js +99 -0
- package/dist/admin/useAdminJob.js.map +1 -0
- package/dist/admin/useAdminStats.d.ts +46 -0
- package/dist/admin/useAdminStats.d.ts.map +1 -0
- package/dist/admin/useAdminStats.js +91 -0
- package/dist/admin/useAdminStats.js.map +1 -0
- package/dist/admin/useBoardGenerator.d.ts +29 -0
- package/dist/admin/useBoardGenerator.d.ts.map +1 -0
- package/dist/admin/useBoardGenerator.js +38 -0
- package/dist/admin/useBoardGenerator.js.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts +14 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.js +28 -0
- package/dist/admin/useBoardTechniquesUpdater.js.map +1 -0
- package/dist/admin/useExampleCreator.d.ts +29 -0
- package/dist/admin/useExampleCreator.d.ts.map +1 -0
- package/dist/admin/useExampleCreator.js +58 -0
- package/dist/admin/useExampleCreator.js.map +1 -0
- package/dist/admin/useSingleBoardExtractor.d.ts +18 -0
- package/dist/admin/useSingleBoardExtractor.d.ts.map +1 -0
- package/dist/admin/useSingleBoardExtractor.js +19 -0
- package/dist/admin/useSingleBoardExtractor.js.map +1 -0
- package/dist/admin/useTechniqueExtractor.d.ts +26 -0
- package/dist/admin/useTechniqueExtractor.d.ts.map +1 -0
- package/dist/admin/useTechniqueExtractor.js +37 -0
- package/dist/admin/useTechniqueExtractor.js.map +1 -0
- package/dist/context/SudojoApiContext.d.ts +58 -0
- package/dist/context/SudojoApiContext.d.ts.map +1 -0
- package/dist/context/SudojoApiContext.js +52 -0
- package/dist/context/SudojoApiContext.js.map +1 -0
- package/dist/context/SudojoApiProvider.d.ts +21 -0
- package/dist/context/SudojoApiProvider.d.ts.map +1 -0
- package/dist/context/SudojoApiProvider.js +15 -0
- package/dist/context/SudojoApiProvider.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -1
- package/dist/hooks/index.d.ts +24 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +22 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useBoardEntry.d.ts +70 -6
- package/dist/hooks/useBoardEntry.d.ts.map +1 -1
- package/dist/hooks/useBoardEntry.js +128 -28
- package/dist/hooks/useBoardEntry.js.map +1 -1
- package/dist/hooks/useBoardScan.d.ts +27 -0
- package/dist/hooks/useBoardScan.d.ts.map +1 -0
- package/dist/hooks/useBoardScan.js +27 -0
- package/dist/hooks/useBoardScan.js.map +1 -0
- package/dist/hooks/useCommunities.d.ts +6 -4
- package/dist/hooks/useCommunities.d.ts.map +1 -1
- package/dist/hooks/useCommunities.js +6 -1
- package/dist/hooks/useCommunities.js.map +1 -1
- package/dist/hooks/useCompletionTrigger.d.ts +23 -0
- package/dist/hooks/useCompletionTrigger.d.ts.map +1 -0
- package/dist/hooks/useCompletionTrigger.js +39 -0
- package/dist/hooks/useCompletionTrigger.js.map +1 -0
- package/dist/hooks/useDailyGame.d.ts +11 -6
- package/dist/hooks/useDailyGame.d.ts.map +1 -1
- package/dist/hooks/useDailyGame.js +20 -61
- package/dist/hooks/useDailyGame.js.map +1 -1
- package/dist/hooks/useDeleteAccount.d.ts +72 -0
- package/dist/hooks/useDeleteAccount.d.ts.map +1 -0
- package/dist/hooks/useDeleteAccount.js +71 -0
- package/dist/hooks/useDeleteAccount.js.map +1 -0
- package/dist/hooks/useEnteredGameSession.d.ts +73 -0
- package/dist/hooks/useEnteredGameSession.d.ts.map +1 -0
- package/dist/hooks/useEnteredGameSession.js +155 -0
- package/dist/hooks/useEnteredGameSession.js.map +1 -0
- package/dist/hooks/useGameSession.d.ts +8 -4
- package/dist/hooks/useGameSession.d.ts.map +1 -1
- package/dist/hooks/useGameSession.js +4 -1
- package/dist/hooks/useGameSession.js.map +1 -1
- package/dist/hooks/useGameTeaching.d.ts.map +1 -1
- package/dist/hooks/useGameTeaching.js +11 -13
- package/dist/hooks/useGameTeaching.js.map +1 -1
- package/dist/hooks/useHint.d.ts +7 -7
- package/dist/hooks/useHint.d.ts.map +1 -1
- package/dist/hooks/useHint.js +18 -9
- package/dist/hooks/useHint.js.map +1 -1
- package/dist/hooks/useIsSiteAdmin.d.ts +39 -0
- package/dist/hooks/useIsSiteAdmin.d.ts.map +1 -0
- package/dist/hooks/useIsSiteAdmin.js +31 -0
- package/dist/hooks/useIsSiteAdmin.js.map +1 -0
- package/dist/hooks/useLearning.d.ts +8 -8
- package/dist/hooks/useLearning.d.ts.map +1 -1
- package/dist/hooks/useLearning.js +5 -2
- package/dist/hooks/useLearning.js.map +1 -1
- package/dist/hooks/useLevelGame.d.ts +7 -7
- package/dist/hooks/useLevelGame.d.ts.map +1 -1
- package/dist/hooks/useLevelGame.js +13 -49
- package/dist/hooks/useLevelGame.js.map +1 -1
- package/dist/hooks/useLevels.d.ts +8 -8
- package/dist/hooks/useLevels.d.ts.map +1 -1
- package/dist/hooks/useLevels.js +7 -3
- package/dist/hooks/useLevels.js.map +1 -1
- package/dist/hooks/usePracticeGame.d.ts +53 -0
- package/dist/hooks/usePracticeGame.d.ts.map +1 -0
- package/dist/hooks/usePracticeGame.js +72 -0
- package/dist/hooks/usePracticeGame.js.map +1 -0
- package/dist/hooks/usePuzzleDistribution.d.ts +2 -0
- package/dist/hooks/usePuzzleDistribution.d.ts.map +1 -1
- package/dist/hooks/usePuzzleDistribution.js +4 -14
- package/dist/hooks/usePuzzleDistribution.js.map +1 -1
- package/dist/hooks/usePuzzleProgress.d.ts +37 -0
- package/dist/hooks/usePuzzleProgress.d.ts.map +1 -0
- package/dist/hooks/usePuzzleProgress.js +48 -0
- package/dist/hooks/usePuzzleProgress.js.map +1 -0
- package/dist/hooks/usePuzzleSession.d.ts +215 -0
- package/dist/hooks/usePuzzleSession.d.ts.map +1 -0
- package/dist/hooks/usePuzzleSession.js +358 -0
- package/dist/hooks/usePuzzleSession.js.map +1 -0
- package/dist/hooks/useResumeGame.d.ts +70 -0
- package/dist/hooks/useResumeGame.d.ts.map +1 -0
- package/dist/hooks/useResumeGame.js +123 -0
- package/dist/hooks/useResumeGame.js.map +1 -0
- package/dist/hooks/useStrategies.d.ts +39 -0
- package/dist/hooks/useStrategies.d.ts.map +1 -0
- package/dist/hooks/useStrategies.js +34 -0
- package/dist/hooks/useStrategies.js.map +1 -0
- package/dist/hooks/useSudoku.d.ts +5 -0
- package/dist/hooks/useSudoku.d.ts.map +1 -1
- package/dist/hooks/useSudoku.js +7 -1
- package/dist/hooks/useSudoku.js.map +1 -1
- package/dist/hooks/useTechniqueByPath.d.ts +46 -0
- package/dist/hooks/useTechniqueByPath.d.ts.map +1 -0
- package/dist/hooks/useTechniqueByPath.js +47 -0
- package/dist/hooks/useTechniqueByPath.js.map +1 -0
- package/dist/hooks/useTechniqueExample.d.ts +60 -0
- package/dist/hooks/useTechniqueExample.d.ts.map +1 -0
- package/dist/hooks/useTechniqueExample.js +63 -0
- package/dist/hooks/useTechniqueExample.js.map +1 -0
- package/dist/hooks/useTechniques.d.ts +8 -8
- package/dist/hooks/useTechniques.d.ts.map +1 -1
- package/dist/hooks/useTechniques.js +9 -21
- package/dist/hooks/useTechniques.js.map +1 -1
- package/dist/index.d.ts +418 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -10
- package/dist/index.js.map +1 -1
- package/dist/types/currentGame.d.ts +16 -1
- package/dist/types/currentGame.d.ts.map +1 -1
- package/dist/types/currentGame.js.map +1 -1
- package/dist/utils/auth.d.ts +6 -0
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +8 -0
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/community.d.ts +64 -0
- package/dist/utils/community.d.ts.map +1 -0
- package/dist/utils/community.js +66 -0
- package/dist/utils/community.js.map +1 -0
- package/dist/utils/date.d.ts +37 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +68 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/entityTranslate.d.ts +120 -0
- package/dist/utils/entityTranslate.d.ts.map +1 -0
- package/dist/utils/entityTranslate.js +146 -0
- package/dist/utils/entityTranslate.js.map +1 -0
- package/dist/utils/gameFetchStatus.d.ts +74 -0
- package/dist/utils/gameFetchStatus.d.ts.map +1 -0
- package/dist/utils/gameFetchStatus.js +118 -0
- package/dist/utils/gameFetchStatus.js.map +1 -0
- package/dist/utils/gamification.d.ts +36 -0
- package/dist/utils/gamification.d.ts.map +1 -0
- package/dist/utils/gamification.js +33 -0
- package/dist/utils/gamification.js.map +1 -0
- package/dist/utils/hintAccess.d.ts +64 -0
- package/dist/utils/hintAccess.d.ts.map +1 -0
- package/dist/utils/hintAccess.js +57 -0
- package/dist/utils/hintAccess.js.map +1 -0
- package/dist/utils/index.d.ts +31 -10
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +20 -9
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/language.d.ts +123 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +95 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/localizedHint.d.ts +60 -0
- package/dist/utils/localizedHint.d.ts.map +1 -1
- package/dist/utils/localizedHint.js +90 -0
- package/dist/utils/localizedHint.js.map +1 -1
- package/dist/utils/progress.d.ts +10 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/scannedBoard.d.ts +79 -0
- package/dist/utils/scannedBoard.d.ts.map +1 -0
- package/dist/utils/scannedBoard.js +96 -0
- package/dist/utils/scannedBoard.js.map +1 -0
- package/dist/utils/shareUrl.d.ts +38 -15
- package/dist/utils/shareUrl.d.ts.map +1 -1
- package/dist/utils/shareUrl.js +75 -26
- package/dist/utils/shareUrl.js.map +1 -1
- package/dist/utils/subscription.d.ts +118 -0
- package/dist/utils/subscription.d.ts.map +1 -1
- package/dist/utils/subscription.js +212 -0
- package/dist/utils/subscription.js.map +1 -1
- package/dist/utils/sudokuPresenter.d.ts +42 -0
- package/dist/utils/sudokuPresenter.d.ts.map +1 -1
- package/dist/utils/sudokuPresenter.js +114 -1
- package/dist/utils/sudokuPresenter.js.map +1 -1
- package/dist/utils/sudokuScrambler.d.ts +8 -0
- package/dist/utils/sudokuScrambler.d.ts.map +1 -1
- package/dist/utils/sudokuScrambler.js +12 -0
- package/dist/utils/sudokuScrambler.js.map +1 -1
- package/dist/utils/technique.d.ts +112 -1
- package/dist/utils/technique.d.ts.map +1 -1
- package/dist/utils/technique.js +293 -1
- package/dist/utils/technique.js.map +1 -1
- package/dist/utils/techniqueBitmask.d.ts +31 -0
- package/dist/utils/techniqueBitmask.d.ts.map +1 -0
- package/dist/utils/techniqueBitmask.js +53 -0
- package/dist/utils/techniqueBitmask.js.map +1 -0
- package/dist/utils/techniqueExample.d.ts +42 -0
- package/dist/utils/techniqueExample.d.ts.map +1 -0
- package/dist/utils/techniqueExample.js +57 -0
- package/dist/utils/techniqueExample.js.map +1 -0
- package/dist/utils/theme.d.ts +2 -0
- package/dist/utils/theme.d.ts.map +1 -1
- package/dist/utils/theme.js +2 -0
- package/dist/utils/theme.js.map +1 -1
- package/package.json +7 -7
|
@@ -3,49 +3,21 @@
|
|
|
3
3
|
* Handles auth and subscription status with automatic refresh
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
import { useResolvedSudojoApi } from '../context/SudojoApiContext';
|
|
6
7
|
import { useSudojoDailyByDate } from '@sudobility/sudojo_client';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const now = new Date();
|
|
10
|
-
const year = now.getFullYear();
|
|
11
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
12
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
13
|
-
return `${year}-${month}-${day}`;
|
|
14
|
-
}
|
|
15
|
-
/** Check if response indicates auth is required */
|
|
16
|
-
function isAuthRequired(response, error) {
|
|
17
|
-
if (response?.success === false &&
|
|
18
|
-
response.action?.type === 'auth_required') {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
if (error && typeof error === 'object') {
|
|
22
|
-
const err = error;
|
|
23
|
-
if (err.message?.includes('Account required'))
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
/** Check if response indicates subscription is required */
|
|
29
|
-
function isSubscriptionRequired(response, error) {
|
|
30
|
-
if (response?.success === false &&
|
|
31
|
-
response.action?.type === 'subscription_required') {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
if (error && typeof error === 'object') {
|
|
35
|
-
const err = error;
|
|
36
|
-
if (err.message?.includes('Daily limit reached') ||
|
|
37
|
-
err.message?.includes('subscription')) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
8
|
+
import { getUtcDateString, normalizeDailyDate } from '../utils/date';
|
|
9
|
+
import { getGameFetchStatus, } from '../utils/gameFetchStatus';
|
|
43
10
|
/**
|
|
44
11
|
* Hook for fetching today's daily puzzle
|
|
45
12
|
*
|
|
46
13
|
* Automatically refetches when auth token or subscription status changes.
|
|
47
14
|
* Returns status indicating whether auth or subscription is required.
|
|
48
15
|
*
|
|
16
|
+
* "Today" is the **UTC** date (getUtcDateString), the same day the API's
|
|
17
|
+
* `/dailies/today` serves, computed once per mount. (It used to be the local
|
|
18
|
+
* date, which asked for tomorrow's or yesterday's daily near midnight.)
|
|
19
|
+
* `dailyDate` is the daily's own `YYYY-MM-DD`, never timezone-shifted.
|
|
20
|
+
*
|
|
49
21
|
* @param options - Hook options
|
|
50
22
|
* @returns Daily puzzle data and status
|
|
51
23
|
*
|
|
@@ -69,44 +41,31 @@ function isSubscriptionRequired(response, error) {
|
|
|
69
41
|
* ```
|
|
70
42
|
*/
|
|
71
43
|
export function useDailyGame(options) {
|
|
72
|
-
const {
|
|
44
|
+
const { subscriptionActive = false, enabled: _enabled = true } = options;
|
|
45
|
+
const { networkClient, baseUrl, token } = useResolvedSudojoApi(options, 'useDailyGame');
|
|
73
46
|
// Track previous state to detect changes
|
|
74
47
|
const prevStateRef = useRef({
|
|
75
48
|
authToken: token,
|
|
76
49
|
subscriptionActive,
|
|
77
50
|
});
|
|
78
|
-
|
|
51
|
+
// "Today" is the UTC date, the same day the API's /dailies/today serves.
|
|
52
|
+
const todayDate = useMemo(() => getUtcDateString(), []);
|
|
79
53
|
const { data, isLoading, error, refetch } = useSudojoDailyByDate(networkClient, baseUrl, token, todayDate);
|
|
80
54
|
// Determine status based on response
|
|
81
|
-
const status = useMemo(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (isSubscriptionRequired(data, error))
|
|
87
|
-
return 'subscription_required';
|
|
88
|
-
if (error)
|
|
89
|
-
return 'error';
|
|
90
|
-
if (data?.success && data.data)
|
|
91
|
-
return 'success';
|
|
92
|
-
return 'loading';
|
|
93
|
-
}, [isLoading, data, error]);
|
|
55
|
+
const status = useMemo(() => getGameFetchStatus({
|
|
56
|
+
isLoading,
|
|
57
|
+
response: data,
|
|
58
|
+
error,
|
|
59
|
+
}), [isLoading, data, error]);
|
|
94
60
|
const daily = useMemo(() => {
|
|
95
61
|
if (data?.success && data.data) {
|
|
96
62
|
return data.data;
|
|
97
63
|
}
|
|
98
64
|
return null;
|
|
99
65
|
}, [data]);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const year = d.getFullYear();
|
|
104
|
-
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
105
|
-
const day = String(d.getDate()).padStart(2, '0');
|
|
106
|
-
return `${year}-${month}-${day}`;
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}, [daily]);
|
|
66
|
+
// The daily's calendar date as sent, without a timezone shift
|
|
67
|
+
// (`new Date('YYYY-MM-DD')` is UTC midnight, the previous day west of UTC).
|
|
68
|
+
const dailyDate = useMemo(() => normalizeDailyDate(daily?.date), [daily]);
|
|
110
69
|
// Auto-refresh when auth token or subscription status changes
|
|
111
70
|
useEffect(() => {
|
|
112
71
|
const prev = prevStateRef.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDailyGame.js","sourceRoot":"","sources":["../../src/hooks/useDailyGame.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"useDailyGame.js","sourceRoot":"","sources":["../../src/hooks/useDailyGame.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAGL,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AA8BlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,EAAE,kBAAkB,GAAG,KAAK,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACzE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,oBAAoB,CAC5D,OAAO,EACP,cAAc,CACf,CAAC;IAEF,yCAAyC;IACzC,MAAM,YAAY,GAAG,MAAM,CAAC;QAC1B,SAAS,EAAE,KAAK;QAChB,kBAAkB;KACnB,CAAC,CAAC;IAEH,yEAAyE;IACzE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC;IAExD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,oBAAoB,CAC9D,aAAa,EACb,OAAO,EACP,KAAK,EACL,SAAS,CACV,CAAC;IAEF,qCAAqC;IACrC,MAAM,MAAM,GAAG,OAAO,CACpB,GAAoB,EAAE,CACpB,kBAAkB,CAAC;QACjB,SAAS;QACT,QAAQ,EAAE,IAAqC;QAC/C,KAAK;KACN,CAAC,EACJ,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,CACzB,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE;QACzB,IAAI,IAAI,EAAE,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,8DAA8D;IAC9D,4EAA4E;IAC5E,MAAM,SAAS,GAAG,OAAO,CACvB,GAAkB,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,EACpD,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,8DAA8D;IAC9D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC;QAClC,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC;QAC7C,MAAM,mBAAmB,GAAG,CAAC,IAAI,CAAC,kBAAkB,IAAI,kBAAkB,CAAC;QAE3E,IAAI,WAAW,IAAI,mBAAmB,EAAE,CAAC;YACvC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,iCAAiC;QACjC,YAAY,CAAC,OAAO,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAClE,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,CAAC,CAAC,CAAC;IAEzC,OAAO;QACL,KAAK;QACL,SAAS;QACT,MAAM;QACN,SAAS;QACT,KAAK,EAAE,KAAK,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5D,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE;KACzB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Hook for fetching and managing today's daily puzzle\n * Handles auth and subscription status with automatic refresh\n */\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport type { Daily } from '@sudobility/sudojo_types';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useResolvedSudojoApi } from '../context/SudojoApiContext';\nimport { useSudojoDailyByDate } from '@sudobility/sudojo_client';\nimport { getUtcDateString, normalizeDailyDate } from '../utils/date';\nimport {\n type GameFetchResponse,\n type GameFetchStatus,\n getGameFetchStatus,\n} from '../utils/gameFetchStatus';\n\nexport interface UseDailyGameOptions {\n /** Network client for API calls (default: SudojoApiProvider) */\n networkClient?: NetworkClient | undefined;\n /** Base URL for the Sudojo API (default: SudojoApiProvider) */\n baseUrl?: string | undefined;\n /** Access token for authentication */\n token?: string | undefined;\n /** Whether subscription is currently active */\n subscriptionActive?: boolean;\n /** Whether to enable the query */\n enabled?: boolean;\n}\n\nexport interface UseDailyGameResult {\n /** Today's daily puzzle */\n daily: Daily | null;\n /** The date string for this daily (YYYY-MM-DD) */\n dailyDate: string | null;\n /** Current status of the game fetch */\n status: GameFetchStatus;\n /** Whether data is loading */\n isLoading: boolean;\n /** Error if fetch failed */\n error: Error | null;\n /** Refetch the daily puzzle */\n refetch: () => void;\n}\n\n/**\n * Hook for fetching today's daily puzzle\n *\n * Automatically refetches when auth token or subscription status changes.\n * Returns status indicating whether auth or subscription is required.\n *\n * \"Today\" is the **UTC** date (getUtcDateString), the same day the API's\n * `/dailies/today` serves, computed once per mount. (It used to be the local\n * date, which asked for tomorrow's or yesterday's daily near midnight.)\n * `dailyDate` is the daily's own `YYYY-MM-DD`, never timezone-shifted.\n *\n * @param options - Hook options\n * @returns Daily puzzle data and status\n *\n * @example\n * ```tsx\n * function DailyPage() {\n * const { daily, dailyDate, status, refetch } = useDailyGame({\n * networkClient,\n * config,\n * auth,\n * subscriptionActive: subscription.isActive,\n * });\n *\n * if (status === 'loading') return <Loading />;\n * if (status === 'auth_required') return <AuthRequired onLogin={openAuthModal} />;\n * if (status === 'subscription_required') return <SubscriptionPaywall onSuccess={refetch} />;\n * if (status === 'error') return <Error />;\n *\n * return <SudokuGame puzzle={daily.board} solution={daily.solution} />;\n * }\n * ```\n */\nexport function useDailyGame(options: UseDailyGameOptions): UseDailyGameResult {\n const { subscriptionActive = false, enabled: _enabled = true } = options;\n const { networkClient, baseUrl, token } = useResolvedSudojoApi(\n options,\n 'useDailyGame'\n );\n\n // Track previous state to detect changes\n const prevStateRef = useRef({\n authToken: token,\n subscriptionActive,\n });\n\n // \"Today\" is the UTC date, the same day the API's /dailies/today serves.\n const todayDate = useMemo(() => getUtcDateString(), []);\n\n const { data, isLoading, error, refetch } = useSudojoDailyByDate(\n networkClient,\n baseUrl,\n token,\n todayDate\n );\n\n // Determine status based on response\n const status = useMemo(\n (): GameFetchStatus =>\n getGameFetchStatus({\n isLoading,\n response: data as GameFetchResponse | undefined,\n error,\n }),\n [isLoading, data, error]\n );\n\n const daily = useMemo(() => {\n if (data?.success && data.data) {\n return data.data;\n }\n return null;\n }, [data]);\n\n // The daily's calendar date as sent, without a timezone shift\n // (`new Date('YYYY-MM-DD')` is UTC midnight, the previous day west of UTC).\n const dailyDate = useMemo(\n (): string | null => normalizeDailyDate(daily?.date),\n [daily]\n );\n\n // Auto-refresh when auth token or subscription status changes\n useEffect(() => {\n const prev = prevStateRef.current;\n const authChanged = prev.authToken !== token;\n const subscriptionChanged = !prev.subscriptionActive && subscriptionActive;\n\n if (authChanged || subscriptionChanged) {\n refetch();\n }\n\n // Update ref for next comparison\n prevStateRef.current = { authToken: token, subscriptionActive };\n }, [token, subscriptionActive, refetch]);\n\n return {\n daily,\n dailyDate,\n status,\n isLoading,\n error: error ?? (data?.error ? new Error(data.error) : null),\n refetch: () => refetch(),\n };\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for deleting the signed-in user's account: the rule (who may delete)
|
|
3
|
+
* and the delete-then-sign-out sequence. Confirmation dialogs and error
|
|
4
|
+
* alerts stay in the apps.
|
|
5
|
+
*/
|
|
6
|
+
import type { NetworkClient } from '@sudobility/types';
|
|
7
|
+
import type { DeleteUserVariables } from '@sudobility/sudojo_client';
|
|
8
|
+
import { type DeleteAccountBlocker, type DeleteAccountCheck } from '../utils/subscription';
|
|
9
|
+
/** Provider tokens the API uses to revoke Sign in with Apple, etc. */
|
|
10
|
+
export type DeleteAccountProviderTokens = DeleteUserVariables['providerTokens'];
|
|
11
|
+
/** What deleteAccount did. */
|
|
12
|
+
export type DeleteAccountOutcome = {
|
|
13
|
+
status: 'deleted';
|
|
14
|
+
} | {
|
|
15
|
+
status: 'blocked';
|
|
16
|
+
reason: DeleteAccountBlocker;
|
|
17
|
+
} | {
|
|
18
|
+
status: 'failed';
|
|
19
|
+
error: Error;
|
|
20
|
+
};
|
|
21
|
+
export interface UseDeleteAccountOptions {
|
|
22
|
+
/** Network client for API calls (default: SudojoApiProvider) */
|
|
23
|
+
networkClient?: NetworkClient | undefined;
|
|
24
|
+
/** Base URL for the Sudojo API (default: SudojoApiProvider) */
|
|
25
|
+
baseUrl?: string | undefined;
|
|
26
|
+
/** Access token (default: SudojoApiProvider) */
|
|
27
|
+
token?: string | null | undefined;
|
|
28
|
+
/** The signed-in user */
|
|
29
|
+
user: {
|
|
30
|
+
uid?: string | null;
|
|
31
|
+
isAnonymous?: boolean | null;
|
|
32
|
+
} | null | undefined;
|
|
33
|
+
/** Whether the user has an active subscription (must cancel first) */
|
|
34
|
+
subscriptionActive: boolean | null | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Sign out locally after the account is deleted (the API deletes the
|
|
37
|
+
* Firebase user; the client must still drop its session).
|
|
38
|
+
*/
|
|
39
|
+
signOut?: () => void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface UseDeleteAccountResult {
|
|
42
|
+
/** Whether the account may be deleted now, and why not */
|
|
43
|
+
canDelete: DeleteAccountCheck;
|
|
44
|
+
/**
|
|
45
|
+
* Delete the account (after the app's confirmation), then sign out.
|
|
46
|
+
* Never throws: the outcome says whether it was blocked or failed.
|
|
47
|
+
*/
|
|
48
|
+
deleteAccount: (providerTokens?: DeleteAccountProviderTokens) => Promise<DeleteAccountOutcome>;
|
|
49
|
+
/** True while the delete request (and sign-out) is in flight */
|
|
50
|
+
isDeleting: boolean;
|
|
51
|
+
/** The last failure, cleared on the next attempt */
|
|
52
|
+
error: Error | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* const { canDelete, deleteAccount, isDeleting } = useDeleteAccount({
|
|
58
|
+
* user, subscriptionActive: subscription.isActive, signOut,
|
|
59
|
+
* });
|
|
60
|
+
* const onPress = async () => {
|
|
61
|
+
* if (!canDelete.allowed) {
|
|
62
|
+
* if (canDelete.reason === 'active_subscription') alert(t('auth.deleteAccountUnsubscribeFirst'));
|
|
63
|
+
* return;
|
|
64
|
+
* }
|
|
65
|
+
* if (!confirm(t('auth.deleteAccountConfirm'))) return;
|
|
66
|
+
* const outcome = await deleteAccount();
|
|
67
|
+
* if (outcome.status === 'failed') alert(outcome.error.message);
|
|
68
|
+
* };
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function useDeleteAccount(options: UseDeleteAccountOptions): UseDeleteAccountResult;
|
|
72
|
+
//# sourceMappingURL=useDeleteAccount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDeleteAccount.d.ts","sourceRoot":"","sources":["../../src/hooks/useDeleteAccount.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAE/B,sEAAsE;AACtE,MAAM,MAAM,2BAA2B,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;AAEhF,8BAA8B;AAC9B,MAAM,MAAM,oBAAoB,GAC5B;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACnD;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvC,MAAM,WAAW,uBAAuB;IACtC,gEAAgE;IAChE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,yBAAyB;IACzB,IAAI,EACA;QAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,GACrD,IAAI,GACJ,SAAS,CAAC;IACd,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,SAAS,EAAE,kBAAkB,CAAC;IAC9B;;;OAGG;IACH,aAAa,EAAE,CACb,cAAc,CAAC,EAAE,2BAA2B,KACzC,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,gEAAgE;IAChE,UAAU,EAAE,OAAO,CAAC;IACpB,oDAAoD;IACpD,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,GAC/B,sBAAsB,CAuDxB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for deleting the signed-in user's account: the rule (who may delete)
|
|
3
|
+
* and the delete-then-sign-out sequence. Confirmation dialogs and error
|
|
4
|
+
* alerts stay in the apps.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
import { useSudojoDeleteUser } from '@sudobility/sudojo_client';
|
|
8
|
+
import { useResolvedSudojoApi } from '../context/SudojoApiContext';
|
|
9
|
+
import { canDeleteAccount, } from '../utils/subscription';
|
|
10
|
+
/**
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { canDelete, deleteAccount, isDeleting } = useDeleteAccount({
|
|
14
|
+
* user, subscriptionActive: subscription.isActive, signOut,
|
|
15
|
+
* });
|
|
16
|
+
* const onPress = async () => {
|
|
17
|
+
* if (!canDelete.allowed) {
|
|
18
|
+
* if (canDelete.reason === 'active_subscription') alert(t('auth.deleteAccountUnsubscribeFirst'));
|
|
19
|
+
* return;
|
|
20
|
+
* }
|
|
21
|
+
* if (!confirm(t('auth.deleteAccountConfirm'))) return;
|
|
22
|
+
* const outcome = await deleteAccount();
|
|
23
|
+
* if (outcome.status === 'failed') alert(outcome.error.message);
|
|
24
|
+
* };
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function useDeleteAccount(options) {
|
|
28
|
+
const { user, subscriptionActive, signOut } = options;
|
|
29
|
+
const { networkClient, baseUrl, token } = useResolvedSudojoApi(options, 'useDeleteAccount');
|
|
30
|
+
const { mutateAsync } = useSudojoDeleteUser(networkClient, baseUrl);
|
|
31
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const canDelete = useMemo(() => canDeleteAccount({ user, subscriptionActive }), [user, subscriptionActive]);
|
|
34
|
+
const deleteAccount = useCallback(async (providerTokens) => {
|
|
35
|
+
if (!canDelete.allowed) {
|
|
36
|
+
return { status: 'blocked', reason: canDelete.reason };
|
|
37
|
+
}
|
|
38
|
+
const userId = user?.uid ?? '';
|
|
39
|
+
setIsDeleting(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
await mutateAsync({
|
|
43
|
+
token,
|
|
44
|
+
userId,
|
|
45
|
+
...(providerTokens !== undefined && { providerTokens }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const failure = err instanceof Error
|
|
50
|
+
? err
|
|
51
|
+
: new Error(String(err) || 'Failed to delete account');
|
|
52
|
+
setError(failure);
|
|
53
|
+
setIsDeleting(false);
|
|
54
|
+
return { status: 'failed', error: failure };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// The account is gone either way; a failed local sign-out does not
|
|
58
|
+
// make the deletion a failure.
|
|
59
|
+
await signOut?.();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error('[useDeleteAccount] signOut failed:', err);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setIsDeleting(false);
|
|
66
|
+
}
|
|
67
|
+
return { status: 'deleted' };
|
|
68
|
+
}, [canDelete, user, mutateAsync, token, signOut]);
|
|
69
|
+
return { canDelete, deleteAccount, isDeleting, error };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=useDeleteAccount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDeleteAccount.js","sourceRoot":"","sources":["../../src/hooks/useDeleteAccount.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EACL,gBAAgB,GAGjB,MAAM,uBAAuB,CAAC;AAgD/B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAgC;IAEhC,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACtD,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,oBAAoB,CAC5D,OAAO,EACP,kBAAkB,CACnB,CAAC;IACF,MAAM,EAAE,WAAW,EAAE,GAAG,mBAAmB,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IACpE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAEvD,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,EAAE,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,EACpD,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAC3B,CAAC;IAEF,MAAM,aAAa,GAAG,WAAW,CAC/B,KAAK,EACH,cAA4C,EACb,EAAE;QACjC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;QACzD,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;QAC/B,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC;YACH,MAAM,WAAW,CAAC;gBAChB,KAAK;gBACL,MAAM;gBACN,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,EAAE,cAAc,EAAE,CAAC;aACxD,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GACX,GAAG,YAAY,KAAK;gBAClB,CAAC,CAAC,GAAG;gBACL,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC,CAAC;YAC3D,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClB,aAAa,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC;YACH,mEAAmE;YACnE,+BAA+B;YAC/B,MAAM,OAAO,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;QAC3D,CAAC;gBAAS,CAAC;YACT,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC,EACD,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAC/C,CAAC;IAEF,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AACzD,CAAC","sourcesContent":["/**\n * Hook for deleting the signed-in user's account: the rule (who may delete)\n * and the delete-then-sign-out sequence. Confirmation dialogs and error\n * alerts stay in the apps.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useSudojoDeleteUser } from '@sudobility/sudojo_client';\nimport type { DeleteUserVariables } from '@sudobility/sudojo_client';\nimport { useResolvedSudojoApi } from '../context/SudojoApiContext';\nimport {\n canDeleteAccount,\n type DeleteAccountBlocker,\n type DeleteAccountCheck,\n} from '../utils/subscription';\n\n/** Provider tokens the API uses to revoke Sign in with Apple, etc. */\nexport type DeleteAccountProviderTokens = DeleteUserVariables['providerTokens'];\n\n/** What deleteAccount did. */\nexport type DeleteAccountOutcome =\n | { status: 'deleted' }\n | { status: 'blocked'; reason: DeleteAccountBlocker }\n | { status: 'failed'; error: Error };\n\nexport interface UseDeleteAccountOptions {\n /** Network client for API calls (default: SudojoApiProvider) */\n networkClient?: NetworkClient | undefined;\n /** Base URL for the Sudojo API (default: SudojoApiProvider) */\n baseUrl?: string | undefined;\n /** Access token (default: SudojoApiProvider) */\n token?: string | null | undefined;\n /** The signed-in user */\n user:\n | { uid?: string | null; isAnonymous?: boolean | null }\n | null\n | undefined;\n /** Whether the user has an active subscription (must cancel first) */\n subscriptionActive: boolean | null | undefined;\n /**\n * Sign out locally after the account is deleted (the API deletes the\n * Firebase user; the client must still drop its session).\n */\n signOut?: () => void | Promise<void>;\n}\n\nexport interface UseDeleteAccountResult {\n /** Whether the account may be deleted now, and why not */\n canDelete: DeleteAccountCheck;\n /**\n * Delete the account (after the app's confirmation), then sign out.\n * Never throws: the outcome says whether it was blocked or failed.\n */\n deleteAccount: (\n providerTokens?: DeleteAccountProviderTokens\n ) => Promise<DeleteAccountOutcome>;\n /** True while the delete request (and sign-out) is in flight */\n isDeleting: boolean;\n /** The last failure, cleared on the next attempt */\n error: Error | null;\n}\n\n/**\n * @example\n * ```tsx\n * const { canDelete, deleteAccount, isDeleting } = useDeleteAccount({\n * user, subscriptionActive: subscription.isActive, signOut,\n * });\n * const onPress = async () => {\n * if (!canDelete.allowed) {\n * if (canDelete.reason === 'active_subscription') alert(t('auth.deleteAccountUnsubscribeFirst'));\n * return;\n * }\n * if (!confirm(t('auth.deleteAccountConfirm'))) return;\n * const outcome = await deleteAccount();\n * if (outcome.status === 'failed') alert(outcome.error.message);\n * };\n * ```\n */\nexport function useDeleteAccount(\n options: UseDeleteAccountOptions\n): UseDeleteAccountResult {\n const { user, subscriptionActive, signOut } = options;\n const { networkClient, baseUrl, token } = useResolvedSudojoApi(\n options,\n 'useDeleteAccount'\n );\n const { mutateAsync } = useSudojoDeleteUser(networkClient, baseUrl);\n const [isDeleting, setIsDeleting] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const canDelete = useMemo(\n () => canDeleteAccount({ user, subscriptionActive }),\n [user, subscriptionActive]\n );\n\n const deleteAccount = useCallback(\n async (\n providerTokens?: DeleteAccountProviderTokens\n ): Promise<DeleteAccountOutcome> => {\n if (!canDelete.allowed) {\n return { status: 'blocked', reason: canDelete.reason };\n }\n const userId = user?.uid ?? '';\n setIsDeleting(true);\n setError(null);\n try {\n await mutateAsync({\n token,\n userId,\n ...(providerTokens !== undefined && { providerTokens }),\n });\n } catch (err) {\n const failure =\n err instanceof Error\n ? err\n : new Error(String(err) || 'Failed to delete account');\n setError(failure);\n setIsDeleting(false);\n return { status: 'failed', error: failure };\n }\n try {\n // The account is gone either way; a failed local sign-out does not\n // make the deletion a failure.\n await signOut?.();\n } catch (err) {\n console.error('[useDeleteAccount] signOut failed:', err);\n } finally {\n setIsDeleting(false);\n }\n return { status: 'deleted' };\n },\n [canDelete, user, mutateAsync, token, signOut]\n );\n\n return { canDelete, deleteAccount, isDeleting, error };\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save / resume orchestration for an entered (typed-in or scanned) puzzle,
|
|
3
|
+
* after useBoardEntry validated it. Port of the web EnterBoard's play-mode
|
|
4
|
+
* logic. Entered puzzles get no server session (no gamification), and their
|
|
5
|
+
* completion is not recorded in progress.
|
|
6
|
+
*/
|
|
7
|
+
import type { BoardEntryPlayState, ValidatedPuzzle } from './useBoardEntry';
|
|
8
|
+
import type { PuzzleGameProps } from './usePuzzleSession';
|
|
9
|
+
import { type UseResumeGameResult } from './useResumeGame';
|
|
10
|
+
export interface UseEnteredGameSessionOptions {
|
|
11
|
+
/** useBoardEntry's validatedPuzzle (null until Validate succeeds) */
|
|
12
|
+
validatedPuzzle: ValidatedPuzzle | null | undefined;
|
|
13
|
+
/** useBoardEntry's initialPlayState (scanned input / entry pencilmarks) */
|
|
14
|
+
initialPlayState?: BoardEntryPlayState | undefined;
|
|
15
|
+
/** The resume decision (default: useResumeGame({ source: 'entered' })) */
|
|
16
|
+
resume?: UseResumeGameResult | undefined;
|
|
17
|
+
/** Forwarded to the internal useResumeGame (RN: its onStoreReady state) */
|
|
18
|
+
isStoreHydrated?: boolean | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Called by newGame() to reset the entry board (pass useBoardEntry's
|
|
21
|
+
* `reset`), so the old validatedPuzzle does not start play again.
|
|
22
|
+
*/
|
|
23
|
+
onReset?: () => void;
|
|
24
|
+
}
|
|
25
|
+
export interface UseEnteredGameSessionResult {
|
|
26
|
+
/** Whether the resume decision is made (store hydrated) */
|
|
27
|
+
isReady: boolean;
|
|
28
|
+
/** Whether a saved entered game is being resumed */
|
|
29
|
+
isResuming: boolean;
|
|
30
|
+
/** The puzzle to play (resumed, else validated), or null while entering */
|
|
31
|
+
activePuzzle: ValidatedPuzzle | null;
|
|
32
|
+
/** Whether to show the game (there is an active puzzle) instead of entry */
|
|
33
|
+
isPlaying: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Props for the game component, or null while entering. Always
|
|
36
|
+
* `scramble: false`. Initial state comes from the saved game when resuming,
|
|
37
|
+
* else from initialPlayState.
|
|
38
|
+
*/
|
|
39
|
+
gameProps: PuzzleGameProps | null;
|
|
40
|
+
/** Remount key for the game component (the puzzle string), or null */
|
|
41
|
+
gameKey: string | null;
|
|
42
|
+
/** Web SudokuGame's onScrambledReady: saves the game once per puzzle */
|
|
43
|
+
onScrambledReady: (board: {
|
|
44
|
+
puzzle: string;
|
|
45
|
+
solution: string;
|
|
46
|
+
}) => void;
|
|
47
|
+
/** RN SudokuGame's onBoardReady: same as onScrambledReady */
|
|
48
|
+
onBoardReady: (puzzle: string, solution: string) => void;
|
|
49
|
+
/** The game's onProgressUpdate (throttled save) */
|
|
50
|
+
onProgress: (inputString: string, pencilmarksString: string, isPencilMode: boolean, autoPencilmarks: boolean, elapsedTime: number) => void;
|
|
51
|
+
/** The game's onComplete: clears the saved game (nothing else) */
|
|
52
|
+
onComplete: () => void;
|
|
53
|
+
/** Back to entry: clear the save, stop resuming, call onReset */
|
|
54
|
+
newGame: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* const entry = useBoardEntry();
|
|
60
|
+
* const session = useEnteredGameSession({
|
|
61
|
+
* validatedPuzzle: entry.validatedPuzzle,
|
|
62
|
+
* initialPlayState: entry.initialPlayState,
|
|
63
|
+
* onReset: entry.reset,
|
|
64
|
+
* });
|
|
65
|
+
* if (session.isPlaying) {
|
|
66
|
+
* return <SudokuGame key={session.gameKey} {...session.gameProps}
|
|
67
|
+
* onScrambledReady={session.onScrambledReady} onProgressUpdate={session.onProgress}
|
|
68
|
+
* onComplete={session.onComplete} onNewGame={session.newGame} />;
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function useEnteredGameSession(options: UseEnteredGameSessionOptions): UseEnteredGameSessionResult;
|
|
73
|
+
//# sourceMappingURL=useEnteredGameSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEnteredGameSession.d.ts","sourceRoot":"","sources":["../../src/hooks/useEnteredGameSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAiB,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,4BAA4B;IAC3C,qEAAqE;IACrE,eAAe,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACpD,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACnD,0EAA0E;IAC1E,MAAM,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACzC,2EAA2E;IAC3E,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,2EAA2E;IAC3E,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;IACrC,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wEAAwE;IACxE,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,6DAA6D;IAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,mDAAmD;IACnD,UAAU,EAAE,CACV,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,EACrB,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IACV,kEAAkE;IAClE,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,iEAAiE;IACjE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,2BAA2B,CAsJ7B"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save / resume orchestration for an entered (typed-in or scanned) puzzle,
|
|
3
|
+
* after useBoardEntry validated it. Port of the web EnterBoard's play-mode
|
|
4
|
+
* logic. Entered puzzles get no server session (no gamification), and their
|
|
5
|
+
* completion is not recorded in progress.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { useGamePlay } from './useGamePlay';
|
|
9
|
+
import { useResumeGame } from './useResumeGame';
|
|
10
|
+
/**
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const entry = useBoardEntry();
|
|
14
|
+
* const session = useEnteredGameSession({
|
|
15
|
+
* validatedPuzzle: entry.validatedPuzzle,
|
|
16
|
+
* initialPlayState: entry.initialPlayState,
|
|
17
|
+
* onReset: entry.reset,
|
|
18
|
+
* });
|
|
19
|
+
* if (session.isPlaying) {
|
|
20
|
+
* return <SudokuGame key={session.gameKey} {...session.gameProps}
|
|
21
|
+
* onScrambledReady={session.onScrambledReady} onProgressUpdate={session.onProgress}
|
|
22
|
+
* onComplete={session.onComplete} onNewGame={session.newGame} />;
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useEnteredGameSession(options) {
|
|
27
|
+
const { validatedPuzzle, initialPlayState, onReset } = options;
|
|
28
|
+
const internalResume = useResumeGame({
|
|
29
|
+
source: 'entered',
|
|
30
|
+
slot: 'play',
|
|
31
|
+
...(options.isStoreHydrated !== undefined && {
|
|
32
|
+
isStoreHydrated: options.isStoreHydrated,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
const resume = options.resume ?? internalResume;
|
|
36
|
+
const resumeGame = resume.resumeGame;
|
|
37
|
+
const isResuming = resumeGame !== null;
|
|
38
|
+
const { startGame, updateProgress, clearGame } = useGamePlay({
|
|
39
|
+
slot: 'play',
|
|
40
|
+
});
|
|
41
|
+
const resumedPuzzle = useMemo(() => {
|
|
42
|
+
if (!resumeGame)
|
|
43
|
+
return null;
|
|
44
|
+
// Level and score were saved with the game, since resuming does not
|
|
45
|
+
// validate again.
|
|
46
|
+
const puzzle = {
|
|
47
|
+
puzzle: resumeGame.puzzle,
|
|
48
|
+
solution: resumeGame.solution,
|
|
49
|
+
};
|
|
50
|
+
if (resumeGame.meta.level !== undefined) {
|
|
51
|
+
puzzle.level = resumeGame.meta.level;
|
|
52
|
+
}
|
|
53
|
+
if (resumeGame.meta.difficultyScore !== undefined) {
|
|
54
|
+
puzzle.difficultyScore = resumeGame.meta.difficultyScore;
|
|
55
|
+
}
|
|
56
|
+
return puzzle;
|
|
57
|
+
}, [resumeGame]);
|
|
58
|
+
const activePuzzle = resumedPuzzle ?? validatedPuzzle ?? null;
|
|
59
|
+
const gameKey = activePuzzle?.puzzle ?? null;
|
|
60
|
+
// --- Save the fresh game once the game reports its board -----------------
|
|
61
|
+
const [reported, setReported] = useState(null);
|
|
62
|
+
const onScrambledReady = useCallback((board) => {
|
|
63
|
+
if (!gameKey)
|
|
64
|
+
return;
|
|
65
|
+
if (board.puzzle.length !== 81 || board.solution.length !== 81)
|
|
66
|
+
return;
|
|
67
|
+
setReported(prev => prev?.key === gameKey &&
|
|
68
|
+
prev.puzzle === board.puzzle &&
|
|
69
|
+
prev.solution === board.solution
|
|
70
|
+
? prev
|
|
71
|
+
: { key: gameKey, ...board });
|
|
72
|
+
}, [gameKey]);
|
|
73
|
+
const onBoardReady = useCallback((puzzle, solution) => {
|
|
74
|
+
onScrambledReady({ puzzle, solution });
|
|
75
|
+
}, [onScrambledReady]);
|
|
76
|
+
const startedRef = useRef('');
|
|
77
|
+
const pencilmarks = initialPlayState?.pencilmarks;
|
|
78
|
+
const autopencil = initialPlayState?.autopencil ?? false;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isResuming || !activePuzzle || !reported)
|
|
81
|
+
return;
|
|
82
|
+
if (reported.key !== gameKey || startedRef.current === gameKey)
|
|
83
|
+
return;
|
|
84
|
+
startedRef.current = gameKey;
|
|
85
|
+
const meta = {};
|
|
86
|
+
if (activePuzzle.level !== undefined)
|
|
87
|
+
meta.level = activePuzzle.level;
|
|
88
|
+
if (activePuzzle.difficultyScore !== undefined) {
|
|
89
|
+
meta.difficultyScore = activePuzzle.difficultyScore;
|
|
90
|
+
}
|
|
91
|
+
startGame('entered', {
|
|
92
|
+
original: reported.puzzle,
|
|
93
|
+
user: reported.puzzle,
|
|
94
|
+
pencilmark: pencilmarks
|
|
95
|
+
? { numbers: pencilmarks, autopencil }
|
|
96
|
+
: { numbers: '', autopencil: false },
|
|
97
|
+
}, reported.solution, meta);
|
|
98
|
+
}, [
|
|
99
|
+
isResuming,
|
|
100
|
+
activePuzzle,
|
|
101
|
+
reported,
|
|
102
|
+
gameKey,
|
|
103
|
+
pencilmarks,
|
|
104
|
+
autopencil,
|
|
105
|
+
startGame,
|
|
106
|
+
]);
|
|
107
|
+
const discardResume = resume.discard;
|
|
108
|
+
const onComplete = useCallback(() => {
|
|
109
|
+
clearGame();
|
|
110
|
+
}, [clearGame]);
|
|
111
|
+
const newGame = useCallback(() => {
|
|
112
|
+
clearGame();
|
|
113
|
+
discardResume();
|
|
114
|
+
setReported(null);
|
|
115
|
+
startedRef.current = '';
|
|
116
|
+
onReset?.();
|
|
117
|
+
}, [clearGame, discardResume, onReset]);
|
|
118
|
+
const gameProps = useMemo(() => {
|
|
119
|
+
if (!activePuzzle)
|
|
120
|
+
return null;
|
|
121
|
+
if (resumeGame) {
|
|
122
|
+
return {
|
|
123
|
+
puzzle: activePuzzle.puzzle,
|
|
124
|
+
solution: activePuzzle.solution,
|
|
125
|
+
scramble: false,
|
|
126
|
+
initialInput: resumeGame.inputString,
|
|
127
|
+
initialPencilmarks: resumeGame.pencilmarksString,
|
|
128
|
+
initialElapsedTime: resumeGame.elapsedTime,
|
|
129
|
+
initialAutoPencilmarks: resumeGame.autoPencilmarks,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
puzzle: activePuzzle.puzzle,
|
|
134
|
+
solution: activePuzzle.solution,
|
|
135
|
+
scramble: false,
|
|
136
|
+
initialInput: initialPlayState?.input,
|
|
137
|
+
initialPencilmarks: initialPlayState?.pencilmarks,
|
|
138
|
+
initialAutoPencilmarks: initialPlayState?.autopencil,
|
|
139
|
+
};
|
|
140
|
+
}, [activePuzzle, resumeGame, initialPlayState]);
|
|
141
|
+
return {
|
|
142
|
+
isReady: resume.isReady,
|
|
143
|
+
isResuming,
|
|
144
|
+
activePuzzle,
|
|
145
|
+
isPlaying: activePuzzle !== null,
|
|
146
|
+
gameProps,
|
|
147
|
+
gameKey,
|
|
148
|
+
onScrambledReady,
|
|
149
|
+
onBoardReady,
|
|
150
|
+
onProgress: updateProgress,
|
|
151
|
+
onComplete,
|
|
152
|
+
newGame,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=useEnteredGameSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEnteredGameSession.js","sourceRoot":"","sources":["../../src/hooks/useEnteredGameSession.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Save / resume orchestration for an entered (typed-in or scanned) puzzle,\n * after useBoardEntry validated it. Port of the web EnterBoard's play-mode\n * logic. Entered puzzles get no server session (no gamification), and their\n * completion is not recorded in progress.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { CurrentGameMeta } from '../types/currentGame';\nimport type { BoardEntryPlayState, ValidatedPuzzle } from './useBoardEntry';\nimport { useGamePlay } from './useGamePlay';\nimport type { PuzzleGameProps } from './usePuzzleSession';\nimport { useResumeGame, type UseResumeGameResult } from './useResumeGame';\n\nexport interface UseEnteredGameSessionOptions {\n /** useBoardEntry's validatedPuzzle (null until Validate succeeds) */\n validatedPuzzle: ValidatedPuzzle | null | undefined;\n /** useBoardEntry's initialPlayState (scanned input / entry pencilmarks) */\n initialPlayState?: BoardEntryPlayState | undefined;\n /** The resume decision (default: useResumeGame({ source: 'entered' })) */\n resume?: UseResumeGameResult | undefined;\n /** Forwarded to the internal useResumeGame (RN: its onStoreReady state) */\n isStoreHydrated?: boolean | undefined;\n /**\n * Called by newGame() to reset the entry board (pass useBoardEntry's\n * `reset`), so the old validatedPuzzle does not start play again.\n */\n onReset?: () => void;\n}\n\nexport interface UseEnteredGameSessionResult {\n /** Whether the resume decision is made (store hydrated) */\n isReady: boolean;\n /** Whether a saved entered game is being resumed */\n isResuming: boolean;\n /** The puzzle to play (resumed, else validated), or null while entering */\n activePuzzle: ValidatedPuzzle | null;\n /** Whether to show the game (there is an active puzzle) instead of entry */\n isPlaying: boolean;\n /**\n * Props for the game component, or null while entering. Always\n * `scramble: false`. Initial state comes from the saved game when resuming,\n * else from initialPlayState.\n */\n gameProps: PuzzleGameProps | null;\n /** Remount key for the game component (the puzzle string), or null */\n gameKey: string | null;\n /** Web SudokuGame's onScrambledReady: saves the game once per puzzle */\n onScrambledReady: (board: { puzzle: string; solution: string }) => void;\n /** RN SudokuGame's onBoardReady: same as onScrambledReady */\n onBoardReady: (puzzle: string, solution: string) => void;\n /** The game's onProgressUpdate (throttled save) */\n onProgress: (\n inputString: string,\n pencilmarksString: string,\n isPencilMode: boolean,\n autoPencilmarks: boolean,\n elapsedTime: number\n ) => void;\n /** The game's onComplete: clears the saved game (nothing else) */\n onComplete: () => void;\n /** Back to entry: clear the save, stop resuming, call onReset */\n newGame: () => void;\n}\n\n/**\n * @example\n * ```tsx\n * const entry = useBoardEntry();\n * const session = useEnteredGameSession({\n * validatedPuzzle: entry.validatedPuzzle,\n * initialPlayState: entry.initialPlayState,\n * onReset: entry.reset,\n * });\n * if (session.isPlaying) {\n * return <SudokuGame key={session.gameKey} {...session.gameProps}\n * onScrambledReady={session.onScrambledReady} onProgressUpdate={session.onProgress}\n * onComplete={session.onComplete} onNewGame={session.newGame} />;\n * }\n * ```\n */\nexport function useEnteredGameSession(\n options: UseEnteredGameSessionOptions\n): UseEnteredGameSessionResult {\n const { validatedPuzzle, initialPlayState, onReset } = options;\n\n const internalResume = useResumeGame({\n source: 'entered',\n slot: 'play',\n ...(options.isStoreHydrated !== undefined && {\n isStoreHydrated: options.isStoreHydrated,\n }),\n });\n const resume = options.resume ?? internalResume;\n const resumeGame = resume.resumeGame;\n const isResuming = resumeGame !== null;\n\n const { startGame, updateProgress, clearGame } = useGamePlay({\n slot: 'play',\n });\n\n const resumedPuzzle = useMemo((): ValidatedPuzzle | null => {\n if (!resumeGame) return null;\n // Level and score were saved with the game, since resuming does not\n // validate again.\n const puzzle: ValidatedPuzzle = {\n puzzle: resumeGame.puzzle,\n solution: resumeGame.solution,\n };\n if (resumeGame.meta.level !== undefined) {\n puzzle.level = resumeGame.meta.level;\n }\n if (resumeGame.meta.difficultyScore !== undefined) {\n puzzle.difficultyScore = resumeGame.meta.difficultyScore;\n }\n return puzzle;\n }, [resumeGame]);\n\n const activePuzzle = resumedPuzzle ?? validatedPuzzle ?? null;\n const gameKey = activePuzzle?.puzzle ?? null;\n\n // --- Save the fresh game once the game reports its board -----------------\n const [reported, setReported] = useState<{\n key: string;\n puzzle: string;\n solution: string;\n } | null>(null);\n\n const onScrambledReady = useCallback(\n (board: { puzzle: string; solution: string }) => {\n if (!gameKey) return;\n if (board.puzzle.length !== 81 || board.solution.length !== 81) return;\n setReported(prev =>\n prev?.key === gameKey &&\n prev.puzzle === board.puzzle &&\n prev.solution === board.solution\n ? prev\n : { key: gameKey, ...board }\n );\n },\n [gameKey]\n );\n\n const onBoardReady = useCallback(\n (puzzle: string, solution: string) => {\n onScrambledReady({ puzzle, solution });\n },\n [onScrambledReady]\n );\n\n const startedRef = useRef('');\n const pencilmarks = initialPlayState?.pencilmarks;\n const autopencil = initialPlayState?.autopencil ?? false;\n useEffect(() => {\n if (isResuming || !activePuzzle || !reported) return;\n if (reported.key !== gameKey || startedRef.current === gameKey) return;\n startedRef.current = gameKey;\n const meta: CurrentGameMeta = {};\n if (activePuzzle.level !== undefined) meta.level = activePuzzle.level;\n if (activePuzzle.difficultyScore !== undefined) {\n meta.difficultyScore = activePuzzle.difficultyScore;\n }\n startGame(\n 'entered',\n {\n original: reported.puzzle,\n user: reported.puzzle,\n pencilmark: pencilmarks\n ? { numbers: pencilmarks, autopencil }\n : { numbers: '', autopencil: false },\n },\n reported.solution,\n meta\n );\n }, [\n isResuming,\n activePuzzle,\n reported,\n gameKey,\n pencilmarks,\n autopencil,\n startGame,\n ]);\n\n const discardResume = resume.discard;\n const onComplete = useCallback(() => {\n clearGame();\n }, [clearGame]);\n\n const newGame = useCallback(() => {\n clearGame();\n discardResume();\n setReported(null);\n startedRef.current = '';\n onReset?.();\n }, [clearGame, discardResume, onReset]);\n\n const gameProps = useMemo((): PuzzleGameProps | null => {\n if (!activePuzzle) return null;\n if (resumeGame) {\n return {\n puzzle: activePuzzle.puzzle,\n solution: activePuzzle.solution,\n scramble: false,\n initialInput: resumeGame.inputString,\n initialPencilmarks: resumeGame.pencilmarksString,\n initialElapsedTime: resumeGame.elapsedTime,\n initialAutoPencilmarks: resumeGame.autoPencilmarks,\n };\n }\n return {\n puzzle: activePuzzle.puzzle,\n solution: activePuzzle.solution,\n scramble: false,\n initialInput: initialPlayState?.input,\n initialPencilmarks: initialPlayState?.pencilmarks,\n initialAutoPencilmarks: initialPlayState?.autopencil,\n };\n }, [activePuzzle, resumeGame, initialPlayState]);\n\n return {\n isReady: resume.isReady,\n isResuming,\n activePuzzle,\n isPlaying: activePuzzle !== null,\n gameProps,\n gameKey,\n onScrambledReady,\n onBoardReady,\n onProgress: updateProgress,\n onComplete,\n newGame,\n };\n}\n"]}
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
import type { GameFinishResponse, GameStartRequest, LocalizedHint } from '@sudobility/sudojo_types';
|
|
10
10
|
import type { NetworkClient } from '@sudobility/types';
|
|
11
11
|
export interface UseGameSessionOptions {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/** Network client for API calls (default: SudojoApiProvider) */
|
|
13
|
+
networkClient?: NetworkClient | undefined;
|
|
14
|
+
/** Base URL for the Sudojo API (default: SudojoApiProvider) */
|
|
15
|
+
baseUrl?: string | undefined;
|
|
16
|
+
/** Access token; null = signed out (default: SudojoApiProvider) */
|
|
17
|
+
token?: string | null | undefined;
|
|
18
|
+
/** Signed-in (non-anonymous) user id, or null */
|
|
15
19
|
userId: string | null;
|
|
16
20
|
}
|
|
17
21
|
export interface GameSessionResult {
|
|
@@ -38,7 +42,7 @@ export interface GameFinishResult {
|
|
|
38
42
|
/** Total points earned */
|
|
39
43
|
totalPointsEarned: number;
|
|
40
44
|
}
|
|
41
|
-
export declare function useGameSession(
|
|
45
|
+
export declare function useGameSession(options: UseGameSessionOptions): {
|
|
42
46
|
startSession: (data: GameStartRequest) => Promise<GameSessionResult>;
|
|
43
47
|
finishSession: (elapsedTime: number) => Promise<GameFinishResult>;
|
|
44
48
|
isStarting: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGameSession.d.ts","sourceRoot":"","sources":["../../src/hooks/useGameSession.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"useGameSession.d.ts","sourceRoot":"","sources":["../../src/hooks/useGameSession.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,iDAAiD;IACjD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,cAAc,EAAE,OAAO,CAAC;IACxB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,SAAS,EAAE,KAAK,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,YAAY,CAAC,EAAE;YACb,KAAK,CAAC,EAAE,aAAa,CAAC;YACtB,WAAW,CAAC,EAAE,aAAa,CAAC;SAC7B,CAAC;KACH,CAAC,CAAC;IACH,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB;yBA8B5C,gBAAgB,KAAG,OAAO,CAAC,iBAAiB,CAAC;iCAsCtC,MAAM,KAAG,OAAO,CAAC,gBAAgB,CAAC;;;;;EA4DzD"}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
10
|
import { useSudojoPlayFinish, useSudojoPlayStart, } from '@sudobility/sudojo_client';
|
|
11
|
-
|
|
11
|
+
import { useResolvedSudojoApi } from '../context/SudojoApiContext';
|
|
12
|
+
export function useGameSession(options) {
|
|
13
|
+
const { userId } = options;
|
|
14
|
+
const { networkClient, baseUrl, token } = useResolvedSudojoApi(options, 'useGameSession');
|
|
12
15
|
const [isStarting, setIsStarting] = useState(false);
|
|
13
16
|
const [isFinishing, setIsFinishing] = useState(false);
|
|
14
17
|
const sessionIdRef = useRef(null);
|