@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
package/dist/utils/shareUrl.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Share URL builder and parser for Sudojo
|
|
2
|
+
* Share URL builder and parser for Sudojo sharing.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Paths match the web app's routes (the RN app's deep-link config accepts the
|
|
5
|
+
* same paths):
|
|
6
|
+
*
|
|
7
|
+
* | type | URL |
|
|
8
|
+
* |--------------|---------------------------------------------------------|
|
|
9
|
+
* | `daily` | `/play/daily` (bare `/daily` has no web route) |
|
|
10
|
+
* | `puzzle` | `/play/puzzle?level=&original=&user=&autopencilmarks=&pencilmarks=&hint=` |
|
|
11
|
+
* | `levels` | `/play` (the level list) |
|
|
12
|
+
* | `play` | `/play/<level>`, or `/play` without a level |
|
|
13
|
+
* | `enter` | `/play/enter` |
|
|
14
|
+
* | `techniques` | `/techniques` |
|
|
15
|
+
* | `technique` | `/techniques/<canonical path>[?hint=<step>]` |
|
|
6
16
|
*
|
|
7
17
|
* Pencilmarks are appended as a raw comma-separated string (not URL-encoded)
|
|
8
18
|
* to keep URLs readable.
|
|
9
19
|
*/
|
|
20
|
+
import { isValidLevel } from '@sudobility/sudojo_types';
|
|
21
|
+
import { toCanonicalTechniquePath } from './technique';
|
|
10
22
|
const DEFAULT_DOMAIN = 'https://sudojo.com';
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* Pencilmarks are appended directly to the URL (without URLSearchParams
|
|
15
|
-
* encoding) so commas remain as literal commas.
|
|
16
|
-
*/
|
|
17
|
-
export function buildShareUrl(params) {
|
|
18
|
-
const domain = params.domain ?? DEFAULT_DOMAIN;
|
|
19
|
-
if (params.type === 'daily') {
|
|
20
|
-
return `${domain}/daily`;
|
|
21
|
-
}
|
|
23
|
+
/** An empty 81-cell input string. */
|
|
24
|
+
const EMPTY_INPUT = '0'.repeat(81);
|
|
25
|
+
function buildPuzzleUrl(domain, params) {
|
|
22
26
|
const searchParams = new URLSearchParams();
|
|
23
27
|
if (params.level != null) {
|
|
24
28
|
searchParams.set('level', String(params.level));
|
|
@@ -43,30 +47,75 @@ export function buildShareUrl(params) {
|
|
|
43
47
|
}
|
|
44
48
|
return url;
|
|
45
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a share URL. See the table at the top of this file for each type.
|
|
52
|
+
*
|
|
53
|
+
* For `puzzle`, pencilmarks are appended directly to the URL (without
|
|
54
|
+
* URLSearchParams encoding) so commas remain as literal commas.
|
|
55
|
+
*/
|
|
56
|
+
export function buildShareUrl(params) {
|
|
57
|
+
const domain = params.domain ?? DEFAULT_DOMAIN;
|
|
58
|
+
switch (params.type) {
|
|
59
|
+
case 'daily':
|
|
60
|
+
return `${domain}/play/daily`;
|
|
61
|
+
case 'levels':
|
|
62
|
+
return `${domain}/play`;
|
|
63
|
+
case 'play':
|
|
64
|
+
return params.level != null
|
|
65
|
+
? `${domain}/play/${params.level}`
|
|
66
|
+
: `${domain}/play`;
|
|
67
|
+
case 'enter':
|
|
68
|
+
return `${domain}/play/enter`;
|
|
69
|
+
case 'techniques':
|
|
70
|
+
return `${domain}/techniques`;
|
|
71
|
+
case 'technique': {
|
|
72
|
+
const slug = toCanonicalTechniquePath(params.path);
|
|
73
|
+
if (!slug)
|
|
74
|
+
return `${domain}/techniques`;
|
|
75
|
+
const hint = params.hint != null ? `?hint=${params.hint}` : '';
|
|
76
|
+
return `${domain}/techniques/${encodeURIComponent(slug)}${hint}`;
|
|
77
|
+
}
|
|
78
|
+
case 'puzzle':
|
|
79
|
+
return buildPuzzleUrl(domain, params);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function readParam(source, name) {
|
|
83
|
+
if (source instanceof URLSearchParams)
|
|
84
|
+
return source.get(name);
|
|
85
|
+
return source[name] ?? null;
|
|
86
|
+
}
|
|
46
87
|
/**
|
|
47
88
|
* Parse share URL query parameters into game state.
|
|
48
|
-
*
|
|
89
|
+
*
|
|
90
|
+
* Accepts URLSearchParams (web) or a plain object of strings (RN route
|
|
91
|
+
* params). Returns null if `original` is missing. A missing `user` (a bare
|
|
92
|
+
* puzzle share, e.g. a freshly entered puzzle) becomes 81 zeros.
|
|
93
|
+
* `level` is kept only when it is a valid level (1-12); `hint` only when it
|
|
94
|
+
* is a non-negative integer. When `level` is absent, callers fall back to the
|
|
95
|
+
* level the solver rates the puzzle at (`parsed.level ?? validatedLevel`).
|
|
49
96
|
*/
|
|
50
97
|
export function parseShareParams(params) {
|
|
51
|
-
const original = params
|
|
52
|
-
|
|
53
|
-
if (!original || !user) {
|
|
98
|
+
const original = readParam(params, 'original');
|
|
99
|
+
if (!original) {
|
|
54
100
|
return null;
|
|
55
101
|
}
|
|
56
|
-
const levelStr = params
|
|
57
|
-
const hintStr = params
|
|
102
|
+
const levelStr = readParam(params, 'level');
|
|
103
|
+
const hintStr = readParam(params, 'hint');
|
|
58
104
|
const result = {
|
|
59
105
|
original,
|
|
60
|
-
user,
|
|
61
|
-
pencilmarks: params
|
|
62
|
-
autopencilmarks: params
|
|
106
|
+
user: readParam(params, 'user') || EMPTY_INPUT,
|
|
107
|
+
pencilmarks: readParam(params, 'pencilmarks') ?? '',
|
|
108
|
+
autopencilmarks: readParam(params, 'autopencilmarks') === 'true',
|
|
63
109
|
};
|
|
64
110
|
if (levelStr) {
|
|
65
|
-
|
|
111
|
+
const level = Number(levelStr);
|
|
112
|
+
if (isValidLevel(level)) {
|
|
113
|
+
result.level = level;
|
|
114
|
+
}
|
|
66
115
|
}
|
|
67
|
-
if (hintStr
|
|
116
|
+
if (hintStr) {
|
|
68
117
|
const hintNum = Number(hintStr);
|
|
69
|
-
if (
|
|
118
|
+
if (Number.isInteger(hintNum) && hintNum >= 0) {
|
|
70
119
|
result.hint = hintNum;
|
|
71
120
|
}
|
|
72
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shareUrl.js","sourceRoot":"","sources":["../../src/utils/shareUrl.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Share URL builder and parser for Sudojo puzzle sharing.\n *\n * - Daily puzzles share just `/daily`\n * - Level/enter puzzles share `/play/puzzle?level=...&original=...&user=...&autopencilmarks=...&pencilmarks=...&hint=...`\n *\n * Pencilmarks are appended as a raw comma-separated string (not URL-encoded)\n * to keep URLs readable.\n */\n\nconst DEFAULT_DOMAIN = 'https://sudojo.com';\n\nexport interface ShareUrlParams {\n type: 'daily' | 'puzzle';\n /** 81-char original puzzle string */\n original?: string;\n /** 81-char user input string */\n user?: string;\n /** Comma-separated pencilmarks string (81 cells, 80 commas) */\n pencilmarks?: string;\n /** Whether auto-pencilmarks are enabled */\n autopencilmarks?: boolean;\n /** Difficulty level (1-based) */\n level?: number;\n /** Hint step index (0-based). Only included when hint is active. */\n hint?: number;\n /** Base domain (default: https://sudojo.com) */\n domain?: string;\n}\n\nexport interface ParsedShareParams {\n original: string;\n user: string;\n pencilmarks: string;\n autopencilmarks: boolean;\n level?: number;\n hint?: number;\n}\n\n/**\n * Build a share URL from game state.\n *\n * Pencilmarks are appended directly to the URL (without URLSearchParams\n * encoding) so commas remain as literal commas.\n */\nexport function buildShareUrl(params: ShareUrlParams): string {\n const domain = params.domain ?? DEFAULT_DOMAIN;\n\n if (params.type === 'daily') {\n return `${domain}/daily`;\n }\n\n const searchParams = new URLSearchParams();\n if (params.level != null) {\n searchParams.set('level', String(params.level));\n }\n if (params.original) {\n searchParams.set('original', params.original);\n }\n if (params.user) {\n searchParams.set('user', params.user);\n }\n if (params.autopencilmarks != null) {\n searchParams.set('autopencilmarks', String(params.autopencilmarks));\n }\n\n let url = `${domain}/play/puzzle?${searchParams.toString()}`;\n\n // Append pencilmarks raw (commas don't need encoding in URLs)\n if (params.pencilmarks != null) {\n url += `&pencilmarks=${params.pencilmarks}`;\n }\n // Append hint step\n if (params.hint != null) {\n url += `&hint=${params.hint}`;\n }\n\n return url;\n}\n\n/**\n * Parse share URL query parameters into game state.\n * Returns null if required params (original, user) are missing.\n */\nexport function parseShareParams(\n params: URLSearchParams\n): ParsedShareParams | null {\n const original = params.get('original');\n const user = params.get('user');\n\n if (!original || !user) {\n return null;\n }\n\n const levelStr = params.get('level');\n const hintStr = params.get('hint');\n\n const result: ParsedShareParams = {\n original,\n user,\n pencilmarks: params.get('pencilmarks') ?? '',\n autopencilmarks: params.get('autopencilmarks') === 'true',\n };\n if (levelStr) {\n result.level = Number(levelStr);\n }\n if (hintStr != null) {\n const hintNum = Number(hintStr);\n if (!isNaN(hintNum) && hintNum >= 0) {\n result.hint = hintNum;\n }\n }\n return result;\n}\n\n/**\n * Derive the web app URL from an API base URL.\n *\n * - Localhost: kept as-is (web app and API share the same server in dev)\n * - Production: strips the `api.` subdomain\n * (e.g. `https://api.sudojo.com` → `https://sudojo.com`)\n */\nexport function getWebUrl(apiBaseUrl: string): string {\n try {\n const url = new URL(apiBaseUrl);\n if (url.hostname !== 'localhost' && url.hostname.startsWith('api.')) {\n url.hostname = url.hostname.replace(/^api\\./, '');\n }\n return url.origin;\n } catch {\n return DEFAULT_DOMAIN;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shareUrl.js","sourceRoot":"","sources":["../../src/utils/shareUrl.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Share URL builder and parser for Sudojo sharing.\n *\n * Paths match the web app's routes (the RN app's deep-link config accepts the\n * same paths):\n *\n * | type | URL |\n * |--------------|---------------------------------------------------------|\n * | `daily` | `/play/daily` (bare `/daily` has no web route) |\n * | `puzzle` | `/play/puzzle?level=&original=&user=&autopencilmarks=&pencilmarks=&hint=` |\n * | `levels` | `/play` (the level list) |\n * | `play` | `/play/<level>`, or `/play` without a level |\n * | `enter` | `/play/enter` |\n * | `techniques` | `/techniques` |\n * | `technique` | `/techniques/<canonical path>[?hint=<step>]` |\n *\n * Pencilmarks are appended as a raw comma-separated string (not URL-encoded)\n * to keep URLs readable.\n */\n\nimport { isValidLevel } from '@sudobility/sudojo_types';\nimport { toCanonicalTechniquePath } from './technique';\n\nconst DEFAULT_DOMAIN = 'https://sudojo.com';\n\n/** An empty 81-cell input string. */\nconst EMPTY_INPUT = '0'.repeat(81);\n\n/** What a share URL points at. */\nexport type ShareUrlType =\n | 'daily'\n | 'puzzle'\n | 'levels'\n | 'play'\n | 'enter'\n | 'techniques'\n | 'technique';\n\nexport interface ShareUrlParams {\n type: ShareUrlType;\n /** 81-char original puzzle string (`puzzle`) */\n original?: string;\n /** 81-char user input string (`puzzle`) */\n user?: string;\n /** Comma-separated pencilmarks string, 81 cells, 80 commas (`puzzle`) */\n pencilmarks?: string;\n /** Whether auto-pencilmarks are enabled (`puzzle`) */\n autopencilmarks?: boolean;\n /** Difficulty level, 1-based (`puzzle`, `play`) */\n level?: number;\n /** Hint step index, 0-based. Only included when a hint is active (`puzzle`, `technique`). */\n hint?: number;\n /** Technique path, API or canonical; shared as the canonical slug (`technique`) */\n path?: string;\n /** Base domain (default: https://sudojo.com) */\n domain?: string;\n}\n\nexport interface ParsedShareParams {\n original: string;\n /** Player input; 81 zeros when the URL has none (a bare puzzle share). */\n user: string;\n pencilmarks: string;\n autopencilmarks: boolean;\n level?: number;\n hint?: number;\n}\n\n/** Query values from URLSearchParams or a plain object (RN route params). */\nexport type ShareParamsSource =\n | URLSearchParams\n | Record<string, string | null | undefined>;\n\nfunction buildPuzzleUrl(domain: string, params: ShareUrlParams): string {\n const searchParams = new URLSearchParams();\n if (params.level != null) {\n searchParams.set('level', String(params.level));\n }\n if (params.original) {\n searchParams.set('original', params.original);\n }\n if (params.user) {\n searchParams.set('user', params.user);\n }\n if (params.autopencilmarks != null) {\n searchParams.set('autopencilmarks', String(params.autopencilmarks));\n }\n\n let url = `${domain}/play/puzzle?${searchParams.toString()}`;\n\n // Append pencilmarks raw (commas don't need encoding in URLs)\n if (params.pencilmarks != null) {\n url += `&pencilmarks=${params.pencilmarks}`;\n }\n // Append hint step\n if (params.hint != null) {\n url += `&hint=${params.hint}`;\n }\n\n return url;\n}\n\n/**\n * Build a share URL. See the table at the top of this file for each type.\n *\n * For `puzzle`, pencilmarks are appended directly to the URL (without\n * URLSearchParams encoding) so commas remain as literal commas.\n */\nexport function buildShareUrl(params: ShareUrlParams): string {\n const domain = params.domain ?? DEFAULT_DOMAIN;\n\n switch (params.type) {\n case 'daily':\n return `${domain}/play/daily`;\n case 'levels':\n return `${domain}/play`;\n case 'play':\n return params.level != null\n ? `${domain}/play/${params.level}`\n : `${domain}/play`;\n case 'enter':\n return `${domain}/play/enter`;\n case 'techniques':\n return `${domain}/techniques`;\n case 'technique': {\n const slug = toCanonicalTechniquePath(params.path);\n if (!slug) return `${domain}/techniques`;\n const hint = params.hint != null ? `?hint=${params.hint}` : '';\n return `${domain}/techniques/${encodeURIComponent(slug)}${hint}`;\n }\n case 'puzzle':\n return buildPuzzleUrl(domain, params);\n }\n}\n\nfunction readParam(source: ShareParamsSource, name: string): string | null {\n if (source instanceof URLSearchParams) return source.get(name);\n return source[name] ?? null;\n}\n\n/**\n * Parse share URL query parameters into game state.\n *\n * Accepts URLSearchParams (web) or a plain object of strings (RN route\n * params). Returns null if `original` is missing. A missing `user` (a bare\n * puzzle share, e.g. a freshly entered puzzle) becomes 81 zeros.\n * `level` is kept only when it is a valid level (1-12); `hint` only when it\n * is a non-negative integer. When `level` is absent, callers fall back to the\n * level the solver rates the puzzle at (`parsed.level ?? validatedLevel`).\n */\nexport function parseShareParams(\n params: ShareParamsSource\n): ParsedShareParams | null {\n const original = readParam(params, 'original');\n\n if (!original) {\n return null;\n }\n\n const levelStr = readParam(params, 'level');\n const hintStr = readParam(params, 'hint');\n\n const result: ParsedShareParams = {\n original,\n user: readParam(params, 'user') || EMPTY_INPUT,\n pencilmarks: readParam(params, 'pencilmarks') ?? '',\n autopencilmarks: readParam(params, 'autopencilmarks') === 'true',\n };\n if (levelStr) {\n const level = Number(levelStr);\n if (isValidLevel(level)) {\n result.level = level;\n }\n }\n if (hintStr) {\n const hintNum = Number(hintStr);\n if (Number.isInteger(hintNum) && hintNum >= 0) {\n result.hint = hintNum;\n }\n }\n return result;\n}\n\n/**\n * Derive the web app URL from an API base URL.\n *\n * - Localhost: kept as-is (web app and API share the same server in dev)\n * - Production: strips the `api.` subdomain\n * (e.g. `https://api.sudojo.com` → `https://sudojo.com`)\n */\nexport function getWebUrl(apiBaseUrl: string): string {\n try {\n const url = new URL(apiBaseUrl);\n if (url.hostname !== 'localhost' && url.hostname.startsWith('api.')) {\n url.hostname = url.hostname.replace(/^api\\./, '');\n }\n return url.origin;\n } catch {\n return DEFAULT_DOMAIN;\n }\n}\n"]}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* These functions work with RevenueCat SDK types to convert to our domain models.
|
|
5
5
|
* The RevenueCat types are passed as generics to avoid direct SDK dependency.
|
|
6
6
|
*/
|
|
7
|
+
import type { Level } from '@sudobility/sudojo_types';
|
|
7
8
|
import type { Product, Subscription } from '../types/subscription';
|
|
9
|
+
import type { TranslateFunction } from './localizedHint';
|
|
8
10
|
/** RevenueCat Package shape (simplified) */
|
|
9
11
|
interface RevenueCatPackage {
|
|
10
12
|
identifier: string;
|
|
@@ -78,5 +80,121 @@ export declare function isBestValuePlan(period: string | undefined): boolean;
|
|
|
78
80
|
* @returns User-friendly error message
|
|
79
81
|
*/
|
|
80
82
|
export declare function getRevenueCatErrorMessage(errorCode: number): string;
|
|
83
|
+
/**
|
|
84
|
+
* i18n key (in the `subscription` namespace) for a period's unit, or null.
|
|
85
|
+
* `P1W` → `periods.week`, `P1M` → `periods.month`, `P3M` → `periods.quarter`,
|
|
86
|
+
* `P6M` → `periods.halfYear`, `P1Y` / `P12M` → `periods.year`. Named periods
|
|
87
|
+
* (`monthly`, `yearly`, ...) are accepted too.
|
|
88
|
+
*
|
|
89
|
+
* The web paywall's copy had no week and no `P12M` (it showed no unit for
|
|
90
|
+
* them); both are mapped here.
|
|
91
|
+
*/
|
|
92
|
+
export declare function getPeriodLabelKey(period: string | null | undefined): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Price suffix for a period, e.g. `/month`, or '' for an unknown period.
|
|
95
|
+
*
|
|
96
|
+
* @param tSub - Translate function for the `subscription` namespace
|
|
97
|
+
*/
|
|
98
|
+
export declare function getPeriodLabel(tSub: TranslateFunction, period: string | null | undefined): string;
|
|
99
|
+
/**
|
|
100
|
+
* Whether a product is the "best value" plan: an annual period (`P1Y`, `P12M`,
|
|
101
|
+
* `yearly`) or an identifier containing `annual` / `yearly`
|
|
102
|
+
* (case-insensitive). Unifies the web paywall's check with isBestValuePlan.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isBestValueProduct(product: {
|
|
105
|
+
period?: string | null;
|
|
106
|
+
identifier?: string | null;
|
|
107
|
+
}): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Length of a subscription period in months, or null when unknown.
|
|
110
|
+
*
|
|
111
|
+
* Accepts the named periods @sudobility/subscription_lib uses (`weekly` 1/4,
|
|
112
|
+
* `monthly` 1, `quarterly` 3, `yearly` 12, `lifetime` Infinity, matching its
|
|
113
|
+
* `periodToMonths`) and ISO 8601 durations (`P1W`, `P6M`, `P1Y`, `P30D`, ...).
|
|
114
|
+
*/
|
|
115
|
+
export declare function subscriptionPeriodToMonths(period: string | null | undefined): number | null;
|
|
116
|
+
/** Price and period of a plan, for savings calculations. */
|
|
117
|
+
export interface PricedPeriod {
|
|
118
|
+
/** Price in the store currency (major units) */
|
|
119
|
+
price: number;
|
|
120
|
+
/** Period, named (`monthly`) or ISO 8601 (`P1M`) */
|
|
121
|
+
period?: string | null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Percent saved per month by `plan` compared with `base` (e.g. annual vs
|
|
125
|
+
* monthly), rounded; null when not comparable (same or unknown/lifetime
|
|
126
|
+
* period, missing product, zero base price) or when there is no saving.
|
|
127
|
+
* Port of the RN SubscriptionScreen's calcSavingsPercent.
|
|
128
|
+
*/
|
|
129
|
+
export declare function calculateSavingsPercent(base: PricedPeriod | null | undefined, plan: PricedPeriod | null | undefined): number | null;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the active subscription was bought on another platform than this
|
|
132
|
+
* client's (so it must be managed there). False when either side is unknown.
|
|
133
|
+
* Matches the RN SubscriptionScreen's isCrossPlatform.
|
|
134
|
+
*/
|
|
135
|
+
export declare function isCrossPlatformSubscription(purchasePlatform: string | null | undefined, clientPlatform: string | null | undefined): boolean;
|
|
136
|
+
/** Why an account cannot be deleted right now. */
|
|
137
|
+
export type DeleteAccountBlocker =
|
|
138
|
+
/** No signed-in user */
|
|
139
|
+
'not_signed_in'
|
|
140
|
+
/** Anonymous (guest) user: there is no account to delete */
|
|
141
|
+
| 'anonymous'
|
|
142
|
+
/** An active subscription must be cancelled first */
|
|
143
|
+
| 'active_subscription';
|
|
144
|
+
/** Result of canDeleteAccount. */
|
|
145
|
+
export type DeleteAccountCheck = {
|
|
146
|
+
allowed: true;
|
|
147
|
+
} | {
|
|
148
|
+
allowed: false;
|
|
149
|
+
reason: DeleteAccountBlocker;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Whether the user may delete their account now. The rule both apps apply:
|
|
153
|
+
* signed in with a real (non-anonymous) account, and no active subscription
|
|
154
|
+
* ("Please cancel your subscription before deleting your account." —
|
|
155
|
+
* `deleteAccountUnsubscribeFirst`). The API also refuses deletion with an
|
|
156
|
+
* active subscription when RevenueCat is configured.
|
|
157
|
+
*/
|
|
158
|
+
export declare function canDeleteAccount(state: {
|
|
159
|
+
user: {
|
|
160
|
+
uid?: string | null;
|
|
161
|
+
isAnonymous?: boolean | null;
|
|
162
|
+
} | null | undefined;
|
|
163
|
+
subscriptionActive: boolean | null | undefined;
|
|
164
|
+
}): DeleteAccountCheck;
|
|
165
|
+
/**
|
|
166
|
+
* Fraction (0-1) of puzzles a subscription offer unlocks, summed from the
|
|
167
|
+
* levels' `percentage`:
|
|
168
|
+
*
|
|
169
|
+
* - No offerId — only free levels (no subscription required)
|
|
170
|
+
* - `'1_blue_belt'` — free levels + blue_belt levels
|
|
171
|
+
* - Anything else — 1.0 (all puzzles)
|
|
172
|
+
*
|
|
173
|
+
* usePuzzleDistribution is the memoized hook form.
|
|
174
|
+
*/
|
|
175
|
+
export declare function getPuzzleDistribution(levels: ReadonlyArray<Pick<Level, 'entitlement' | 'percentage'>>, offerId?: string | null): number;
|
|
176
|
+
/** A plan as the paywall lists it (price may be a string, as on Product). */
|
|
177
|
+
export interface PaywallPlan {
|
|
178
|
+
/** Price in the store currency (major units), e.g. 4.99 or '4.99' */
|
|
179
|
+
price: number | string;
|
|
180
|
+
/** Period, named (`monthly`) or ISO 8601 (`P1M`) */
|
|
181
|
+
period?: string | null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The plan the savings badge compares with: the one with the shortest known
|
|
185
|
+
* period (usually monthly). Plans with an unknown period are skipped; the
|
|
186
|
+
* first of equal periods wins. Null when no plan has a known period.
|
|
187
|
+
* (Web SubscriptionPaywall's baseProduct.)
|
|
188
|
+
*/
|
|
189
|
+
export declare function selectSavingsBasePlan<T extends {
|
|
190
|
+
period?: string | null;
|
|
191
|
+
}>(products: readonly T[]): T | null;
|
|
192
|
+
/**
|
|
193
|
+
* Percent `plan` saves per month against the savings base plan of
|
|
194
|
+
* `products` (selectSavingsBasePlan), via calculateSavingsPercent; null when
|
|
195
|
+
* not comparable or no saving. The web paywall shows it only on the
|
|
196
|
+
* best-value plan (isBestValueProduct).
|
|
197
|
+
*/
|
|
198
|
+
export declare function getPlanSavingsPercent(products: readonly PaywallPlan[], plan: PaywallPlan): number | null;
|
|
81
199
|
export {};
|
|
82
200
|
//# sourceMappingURL=subscription.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../src/utils/subscription.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../src/utils/subscription.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* These functions work with RevenueCat SDK types to convert to our domain models.
|
|
5
5
|
* The RevenueCat types are passed as generics to avoid direct SDK dependency.
|
|
6
6
|
*/
|
|
7
|
+
import { getSubscriptionOfferId } from '@sudobility/sudojo_types';
|
|
7
8
|
/**
|
|
8
9
|
* Convert RevenueCat package to our Product interface
|
|
9
10
|
*
|
|
@@ -114,4 +115,215 @@ export function getRevenueCatErrorMessage(errorCode) {
|
|
|
114
115
|
return 'Purchase failed. Please try again.';
|
|
115
116
|
}
|
|
116
117
|
}
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Paywall display helpers
|
|
120
|
+
// =============================================================================
|
|
121
|
+
/** Named periods (as @sudobility/subscription_lib reports them) → ISO 8601. */
|
|
122
|
+
const NAMED_PERIOD_TO_ISO = {
|
|
123
|
+
weekly: 'P1W',
|
|
124
|
+
monthly: 'P1M',
|
|
125
|
+
quarterly: 'P3M',
|
|
126
|
+
yearly: 'P1Y',
|
|
127
|
+
};
|
|
128
|
+
function toIsoPeriod(period) {
|
|
129
|
+
if (!period)
|
|
130
|
+
return undefined;
|
|
131
|
+
return NAMED_PERIOD_TO_ISO[period.toLowerCase()] ?? period.toUpperCase();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* i18n key (in the `subscription` namespace) for a period's unit, or null.
|
|
135
|
+
* `P1W` → `periods.week`, `P1M` → `periods.month`, `P3M` → `periods.quarter`,
|
|
136
|
+
* `P6M` → `periods.halfYear`, `P1Y` / `P12M` → `periods.year`. Named periods
|
|
137
|
+
* (`monthly`, `yearly`, ...) are accepted too.
|
|
138
|
+
*
|
|
139
|
+
* The web paywall's copy had no week and no `P12M` (it showed no unit for
|
|
140
|
+
* them); both are mapped here.
|
|
141
|
+
*/
|
|
142
|
+
export function getPeriodLabelKey(period) {
|
|
143
|
+
switch (toIsoPeriod(period)) {
|
|
144
|
+
case 'P1W':
|
|
145
|
+
case 'P7D':
|
|
146
|
+
return 'periods.week';
|
|
147
|
+
case 'P1M':
|
|
148
|
+
return 'periods.month';
|
|
149
|
+
case 'P3M':
|
|
150
|
+
return 'periods.quarter';
|
|
151
|
+
case 'P6M':
|
|
152
|
+
return 'periods.halfYear';
|
|
153
|
+
case 'P1Y':
|
|
154
|
+
case 'P12M':
|
|
155
|
+
return 'periods.year';
|
|
156
|
+
default:
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Price suffix for a period, e.g. `/month`, or '' for an unknown period.
|
|
162
|
+
*
|
|
163
|
+
* @param tSub - Translate function for the `subscription` namespace
|
|
164
|
+
*/
|
|
165
|
+
export function getPeriodLabel(tSub, period) {
|
|
166
|
+
const key = getPeriodLabelKey(period);
|
|
167
|
+
return key ? `/${tSub(key)}` : '';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Whether a product is the "best value" plan: an annual period (`P1Y`, `P12M`,
|
|
171
|
+
* `yearly`) or an identifier containing `annual` / `yearly`
|
|
172
|
+
* (case-insensitive). Unifies the web paywall's check with isBestValuePlan.
|
|
173
|
+
*/
|
|
174
|
+
export function isBestValueProduct(product) {
|
|
175
|
+
const iso = toIsoPeriod(product.period);
|
|
176
|
+
if (iso === 'P1Y' || iso === 'P12M')
|
|
177
|
+
return true;
|
|
178
|
+
const id = product.identifier?.toLowerCase() ?? '';
|
|
179
|
+
return id.includes('annual') || id.includes('yearly');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Length of a subscription period in months, or null when unknown.
|
|
183
|
+
*
|
|
184
|
+
* Accepts the named periods @sudobility/subscription_lib uses (`weekly` 1/4,
|
|
185
|
+
* `monthly` 1, `quarterly` 3, `yearly` 12, `lifetime` Infinity, matching its
|
|
186
|
+
* `periodToMonths`) and ISO 8601 durations (`P1W`, `P6M`, `P1Y`, `P30D`, ...).
|
|
187
|
+
*/
|
|
188
|
+
export function subscriptionPeriodToMonths(period) {
|
|
189
|
+
if (!period)
|
|
190
|
+
return null;
|
|
191
|
+
const lower = period.toLowerCase();
|
|
192
|
+
switch (lower) {
|
|
193
|
+
case 'weekly':
|
|
194
|
+
return 1 / 4;
|
|
195
|
+
case 'monthly':
|
|
196
|
+
return 1;
|
|
197
|
+
case 'quarterly':
|
|
198
|
+
return 3;
|
|
199
|
+
case 'yearly':
|
|
200
|
+
return 12;
|
|
201
|
+
case 'lifetime':
|
|
202
|
+
return Infinity;
|
|
203
|
+
}
|
|
204
|
+
const match = /^P(\d+)([DWMY])$/.exec(period.toUpperCase());
|
|
205
|
+
if (!match?.[1] || !match[2])
|
|
206
|
+
return null;
|
|
207
|
+
const value = parseInt(match[1], 10);
|
|
208
|
+
switch (match[2]) {
|
|
209
|
+
case 'D':
|
|
210
|
+
return value / 30;
|
|
211
|
+
case 'W':
|
|
212
|
+
return value / 4;
|
|
213
|
+
case 'M':
|
|
214
|
+
return value;
|
|
215
|
+
default:
|
|
216
|
+
return value * 12;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Percent saved per month by `plan` compared with `base` (e.g. annual vs
|
|
221
|
+
* monthly), rounded; null when not comparable (same or unknown/lifetime
|
|
222
|
+
* period, missing product, zero base price) or when there is no saving.
|
|
223
|
+
* Port of the RN SubscriptionScreen's calcSavingsPercent.
|
|
224
|
+
*/
|
|
225
|
+
export function calculateSavingsPercent(base, plan) {
|
|
226
|
+
if (!base || !plan)
|
|
227
|
+
return null;
|
|
228
|
+
const baseMonths = subscriptionPeriodToMonths(base.period);
|
|
229
|
+
const planMonths = subscriptionPeriodToMonths(plan.period);
|
|
230
|
+
if (baseMonths == null || planMonths == null)
|
|
231
|
+
return null;
|
|
232
|
+
if (baseMonths <= 0 || planMonths <= 0)
|
|
233
|
+
return null;
|
|
234
|
+
if (!isFinite(baseMonths) || !isFinite(planMonths))
|
|
235
|
+
return null;
|
|
236
|
+
if (baseMonths === planMonths)
|
|
237
|
+
return null;
|
|
238
|
+
const baseMonthlyCost = base.price / baseMonths;
|
|
239
|
+
const planMonthlyCost = plan.price / planMonths;
|
|
240
|
+
if (baseMonthlyCost <= 0)
|
|
241
|
+
return null;
|
|
242
|
+
const savings = Math.round(((baseMonthlyCost - planMonthlyCost) / baseMonthlyCost) * 100);
|
|
243
|
+
return savings > 0 ? savings : null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Whether the active subscription was bought on another platform than this
|
|
247
|
+
* client's (so it must be managed there). False when either side is unknown.
|
|
248
|
+
* Matches the RN SubscriptionScreen's isCrossPlatform.
|
|
249
|
+
*/
|
|
250
|
+
export function isCrossPlatformSubscription(purchasePlatform, clientPlatform) {
|
|
251
|
+
return (!!purchasePlatform &&
|
|
252
|
+
!!clientPlatform &&
|
|
253
|
+
purchasePlatform !== clientPlatform);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether the user may delete their account now. The rule both apps apply:
|
|
257
|
+
* signed in with a real (non-anonymous) account, and no active subscription
|
|
258
|
+
* ("Please cancel your subscription before deleting your account." —
|
|
259
|
+
* `deleteAccountUnsubscribeFirst`). The API also refuses deletion with an
|
|
260
|
+
* active subscription when RevenueCat is configured.
|
|
261
|
+
*/
|
|
262
|
+
export function canDeleteAccount(state) {
|
|
263
|
+
if (!state.user?.uid)
|
|
264
|
+
return { allowed: false, reason: 'not_signed_in' };
|
|
265
|
+
if (state.user.isAnonymous)
|
|
266
|
+
return { allowed: false, reason: 'anonymous' };
|
|
267
|
+
if (state.subscriptionActive) {
|
|
268
|
+
return { allowed: false, reason: 'active_subscription' };
|
|
269
|
+
}
|
|
270
|
+
return { allowed: true };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Fraction (0-1) of puzzles a subscription offer unlocks, summed from the
|
|
274
|
+
* levels' `percentage`:
|
|
275
|
+
*
|
|
276
|
+
* - No offerId — only free levels (no subscription required)
|
|
277
|
+
* - `'1_blue_belt'` — free levels + blue_belt levels
|
|
278
|
+
* - Anything else — 1.0 (all puzzles)
|
|
279
|
+
*
|
|
280
|
+
* usePuzzleDistribution is the memoized hook form.
|
|
281
|
+
*/
|
|
282
|
+
export function getPuzzleDistribution(levels, offerId) {
|
|
283
|
+
if (offerId && offerId !== '1_blue_belt')
|
|
284
|
+
return 1.0;
|
|
285
|
+
let total = 0;
|
|
286
|
+
for (const level of levels) {
|
|
287
|
+
const levelOfferId = getSubscriptionOfferId(level.entitlement);
|
|
288
|
+
if (!levelOfferId ||
|
|
289
|
+
(offerId === '1_blue_belt' && levelOfferId === '1_blue_belt')) {
|
|
290
|
+
total += level.percentage ?? 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return total;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The plan the savings badge compares with: the one with the shortest known
|
|
297
|
+
* period (usually monthly). Plans with an unknown period are skipped; the
|
|
298
|
+
* first of equal periods wins. Null when no plan has a known period.
|
|
299
|
+
* (Web SubscriptionPaywall's baseProduct.)
|
|
300
|
+
*/
|
|
301
|
+
export function selectSavingsBasePlan(products) {
|
|
302
|
+
let base = null;
|
|
303
|
+
let baseMonths = Infinity;
|
|
304
|
+
for (const product of products) {
|
|
305
|
+
const months = subscriptionPeriodToMonths(product.period);
|
|
306
|
+
if (months != null && months < baseMonths) {
|
|
307
|
+
base = product;
|
|
308
|
+
baseMonths = months;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return base;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Percent `plan` saves per month against the savings base plan of
|
|
315
|
+
* `products` (selectSavingsBasePlan), via calculateSavingsPercent; null when
|
|
316
|
+
* not comparable or no saving. The web paywall shows it only on the
|
|
317
|
+
* best-value plan (isBestValueProduct).
|
|
318
|
+
*/
|
|
319
|
+
export function getPlanSavingsPercent(products, plan) {
|
|
320
|
+
const base = selectSavingsBasePlan(products);
|
|
321
|
+
if (!base)
|
|
322
|
+
return null;
|
|
323
|
+
const toPriced = (p) => ({
|
|
324
|
+
price: typeof p.price === 'number' ? p.price : parseFloat(p.price),
|
|
325
|
+
period: p.period ?? null,
|
|
326
|
+
});
|
|
327
|
+
return calculateSavingsPercent(toPriced(base), toPriced(plan));
|
|
328
|
+
}
|
|
117
329
|
//# sourceMappingURL=subscription.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../src/utils/subscription.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subscription utilities\n *\n * These functions work with RevenueCat SDK types to convert to our domain models.\n * The RevenueCat types are passed as generics to avoid direct SDK dependency.\n */\n\nimport type { Product, Subscription } from '../types/subscription';\n\n/** RevenueCat Package shape (simplified) */\ninterface RevenueCatPackage {\n identifier: string;\n rcBillingProduct?: {\n identifier?: string;\n title?: string;\n description?: string;\n currentPrice?: {\n amountMicros?: number;\n formattedPrice?: string;\n };\n normalPeriodDuration?: string;\n defaultSubscriptionOption?: {\n introPrice?: {\n price?: {\n formattedPrice?: string;\n };\n periodDuration?: string;\n };\n trial?: {\n periodDuration?: string;\n };\n };\n };\n}\n\n/** RevenueCat CustomerInfo entitlement shape */\ninterface RevenueCatEntitlement {\n expirationDate?: number | null;\n latestPurchaseDate?: number | null;\n productIdentifier?: string;\n willRenew?: boolean;\n}\n\n/** RevenueCat CustomerInfo shape (simplified) */\ninterface RevenueCatCustomerInfo {\n entitlements: {\n active: Record<string, RevenueCatEntitlement>;\n };\n}\n\n/**\n * Convert RevenueCat package to our Product interface\n *\n * @param pkg - RevenueCat package\n * @returns Product object\n */\nexport function convertPackageToProduct(pkg: RevenueCatPackage): Product {\n const product = pkg.rcBillingProduct;\n const subscriptionOption = product?.defaultSubscriptionOption;\n\n const result: Product = {\n identifier: pkg.identifier,\n price: product?.currentPrice?.amountMicros\n ? (product.currentPrice.amountMicros / 1000000).toFixed(2)\n : '0',\n priceString: product?.currentPrice?.formattedPrice ?? '$0',\n title: product?.title ?? pkg.identifier,\n description: product?.description ?? '',\n };\n\n if (product?.identifier !== undefined) {\n result.productId = product.identifier;\n }\n if (product?.normalPeriodDuration !== undefined) {\n result.period = product.normalPeriodDuration;\n }\n if (subscriptionOption?.introPrice?.price?.formattedPrice !== undefined) {\n result.introPrice = subscriptionOption.introPrice.price.formattedPrice;\n }\n if (subscriptionOption?.introPrice?.periodDuration !== undefined) {\n result.introPricePeriod = subscriptionOption.introPrice.periodDuration;\n }\n if (subscriptionOption?.trial?.periodDuration !== undefined) {\n result.freeTrialPeriod = subscriptionOption.trial.periodDuration;\n }\n\n return result;\n}\n\n/**\n * Parse RevenueCat CustomerInfo into our Subscription interface\n *\n * @param customerInfo - RevenueCat customer info\n * @returns Subscription object\n */\nexport function parseCustomerInfo(\n customerInfo: RevenueCatCustomerInfo\n): Subscription {\n const hasActiveEntitlement =\n Object.keys(customerInfo.entitlements.active).length > 0;\n const activeEntitlement = Object.values(customerInfo.entitlements.active)[0];\n\n if (hasActiveEntitlement && activeEntitlement) {\n const subscription: Subscription = { isActive: true };\n\n if (activeEntitlement.expirationDate != null) {\n subscription.expirationDate = new Date(activeEntitlement.expirationDate);\n }\n if (activeEntitlement.latestPurchaseDate != null) {\n subscription.purchaseDate = new Date(\n activeEntitlement.latestPurchaseDate\n );\n }\n if (activeEntitlement.productIdentifier !== undefined) {\n subscription.productIdentifier = activeEntitlement.productIdentifier;\n }\n if (activeEntitlement.willRenew !== undefined) {\n subscription.willRenew = activeEntitlement.willRenew;\n }\n\n return subscription;\n }\n\n return { isActive: false };\n}\n\n/**\n * Get display name for subscription period\n *\n * @param period - ISO 8601 duration string (e.g., \"P1M\", \"P1Y\")\n * @returns Human-readable period name\n */\nexport function getPeriodDisplayName(period: string | undefined): string {\n if (!period) return '';\n\n const periodMap: Record<string, string> = {\n P1W: 'Weekly',\n P1M: 'Monthly',\n P3M: 'Quarterly',\n P6M: 'Semi-Annual',\n P1Y: 'Annual',\n P12M: 'Annual',\n };\n\n return periodMap[period] ?? period;\n}\n\n/**\n * Check if a plan is the \"best value\" (typically annual plans)\n *\n * @param period - ISO 8601 duration string\n * @returns Whether this is typically the best value option\n */\nexport function isBestValuePlan(period: string | undefined): boolean {\n return period === 'P1Y' || period === 'P12M';\n}\n\n/**\n * Get RevenueCat purchase error message\n *\n * @param errorCode - RevenueCat error code\n * @returns User-friendly error message\n */\nexport function getRevenueCatErrorMessage(errorCode: number): string {\n switch (errorCode) {\n case 1:\n return 'Purchase cancelled';\n case 2:\n return 'Store problem occurred. Please try again later.';\n case 3:\n return 'Purchase not allowed on this device';\n case 7:\n return 'Network error. Please check your connection.';\n default:\n return 'Purchase failed. Please try again.';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../src/utils/subscription.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AA6ClE;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAsB;IAC5D,MAAM,OAAO,GAAG,GAAG,CAAC,gBAAgB,CAAC;IACrC,MAAM,kBAAkB,GAAG,OAAO,EAAE,yBAAyB,CAAC;IAE9D,MAAM,MAAM,GAAY;QACtB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY;YACxC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YAC1D,CAAC,CAAC,GAAG;QACP,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,IAAI,IAAI;QAC1D,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,CAAC,UAAU;QACvC,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE;KACxC,CAAC;IAEF,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IACxC,CAAC;IACD,IAAI,OAAO,EAAE,oBAAoB,KAAK,SAAS,EAAE,CAAC;QAChD,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAC/C,CAAC;IACD,IAAI,kBAAkB,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,KAAK,SAAS,EAAE,CAAC;QACxE,MAAM,CAAC,UAAU,GAAG,kBAAkB,CAAC,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC;IACzE,CAAC;IACD,IAAI,kBAAkB,EAAE,UAAU,EAAE,cAAc,KAAK,SAAS,EAAE,CAAC;QACjE,MAAM,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,UAAU,CAAC,cAAc,CAAC;IACzE,CAAC;IACD,IAAI,kBAAkB,EAAE,KAAK,EAAE,cAAc,KAAK,SAAS,EAAE,CAAC;QAC5D,MAAM,CAAC,eAAe,GAAG,kBAAkB,CAAC,KAAK,CAAC,cAAc,CAAC;IACnE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAoC;IAEpC,MAAM,oBAAoB,GACxB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3D,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,IAAI,oBAAoB,IAAI,iBAAiB,EAAE,CAAC;QAC9C,MAAM,YAAY,GAAiB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAEtD,IAAI,iBAAiB,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;YAC7C,YAAY,CAAC,cAAc,GAAG,IAAI,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,iBAAiB,CAAC,kBAAkB,IAAI,IAAI,EAAE,CAAC;YACjD,YAAY,CAAC,YAAY,GAAG,IAAI,IAAI,CAClC,iBAAiB,CAAC,kBAAkB,CACrC,CAAC;QACJ,CAAC;QACD,IAAI,iBAAiB,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACtD,YAAY,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,iBAAiB,CAAC;QACvE,CAAC;QACD,IAAI,iBAAiB,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC9C,YAAY,CAAC,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC;QACvD,CAAC;QAED,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA0B;IAC7D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,SAAS,GAA2B;QACxC,GAAG,EAAE,QAAQ;QACb,GAAG,EAAE,SAAS;QACd,GAAG,EAAE,WAAW;QAChB,GAAG,EAAE,aAAa;QAClB,GAAG,EAAE,QAAQ;QACb,IAAI,EAAE,QAAQ;KACf,CAAC;IAEF,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAA0B;IACxD,OAAO,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,CAAC;YACJ,OAAO,oBAAoB,CAAC;QAC9B,KAAK,CAAC;YACJ,OAAO,iDAAiD,CAAC;QAC3D,KAAK,CAAC;YACJ,OAAO,qCAAqC,CAAC;QAC/C,KAAK,CAAC;YACJ,OAAO,8CAA8C,CAAC;QACxD;YACE,OAAO,oCAAoC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,0BAA0B;AAC1B,gFAAgF;AAEhF,+EAA+E;AAC/E,MAAM,mBAAmB,GAAqC;IAC5D,MAAM,EAAE,KAAK;IACb,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,KAAK;CACd,CAAC;AAEF,SAAS,WAAW,CAAC,MAAiC;IACpD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,mBAAmB,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;AAC3E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAiC;IAEjC,QAAQ,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,KAAK,KAAK,CAAC;QACX,KAAK,KAAK;YACR,OAAO,cAAc,CAAC;QACxB,KAAK,KAAK;YACR,OAAO,eAAe,CAAC;QACzB,KAAK,KAAK;YACR,OAAO,iBAAiB,CAAC;QAC3B,KAAK,KAAK;YACR,OAAO,kBAAkB,CAAC;QAC5B,KAAK,KAAK,CAAC;QACX,KAAK,MAAM;YACT,OAAO,cAAc,CAAC;QACxB;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAuB,EACvB,MAAiC;IAEjC,MAAM,GAAG,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAGlC;IACC,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACnD,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAiC;IAEjC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACnC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,CAAC,GAAG,CAAC,CAAC;QACf,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC;QACX,KAAK,WAAW;YACd,OAAO,CAAC,CAAC;QACX,KAAK,QAAQ;YACX,OAAO,EAAE,CAAC;QACZ,KAAK,UAAU;YACb,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrC,QAAQ,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACjB,KAAK,GAAG;YACN,OAAO,KAAK,GAAG,EAAE,CAAC;QACpB,KAAK,GAAG;YACN,OAAO,KAAK,GAAG,CAAC,CAAC;QACnB,KAAK,GAAG;YACN,OAAO,KAAK,CAAC;QACf;YACE,OAAO,KAAK,GAAG,EAAE,CAAC;IACtB,CAAC;AACH,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAqC,EACrC,IAAqC;IAErC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,UAAU,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,IAAI,UAAU,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,UAAU,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,IAAI,UAAU,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC;IAChD,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC;IAChD,IAAI,eAAe,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,CAAC,CAAC,eAAe,GAAG,eAAe,CAAC,GAAG,eAAe,CAAC,GAAG,GAAG,CAC9D,CAAC;IACF,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CACzC,gBAA2C,EAC3C,cAAyC;IAEzC,OAAO,CACL,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,cAAc;QAChB,gBAAgB,KAAK,cAAc,CACpC,CAAC;AACJ,CAAC;AAgBD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAMhC;IACC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IACzE,IAAI,KAAK,CAAC,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC3E,IAAI,KAAK,CAAC,kBAAkB,EAAE,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAC3D,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAgE,EAChE,OAAuB;IAEvB,IAAI,OAAO,IAAI,OAAO,KAAK,aAAa;QAAE,OAAO,GAAG,CAAC;IAErD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,YAAY,GAAG,sBAAsB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/D,IACE,CAAC,YAAY;YACb,CAAC,OAAO,KAAK,aAAa,IAAI,YAAY,KAAK,aAAa,CAAC,EAC7D,CAAC;YACD,KAAK,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAsB;IAEtB,IAAI,IAAI,GAAa,IAAI,CAAC;IAC1B,IAAI,UAAU,GAAG,QAAQ,CAAC;IAC1B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,0BAA0B,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC1D,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;YAC1C,IAAI,GAAG,OAAO,CAAC;YACf,UAAU,GAAG,MAAM,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgC,EAChC,IAAiB;IAEjB,MAAM,IAAI,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,QAAQ,GAAG,CAAC,CAAc,EAAgB,EAAE,CAAC,CAAC;QAClD,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;QAClE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI;KACzB,CAAC,CAAC;IACH,OAAO,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC","sourcesContent":["/**\n * Subscription utilities\n *\n * These functions work with RevenueCat SDK types to convert to our domain models.\n * The RevenueCat types are passed as generics to avoid direct SDK dependency.\n */\n\nimport type { Level } from '@sudobility/sudojo_types';\nimport { getSubscriptionOfferId } from '@sudobility/sudojo_types';\nimport type { Product, Subscription } from '../types/subscription';\nimport type { TranslateFunction } from './localizedHint';\n\n/** RevenueCat Package shape (simplified) */\ninterface RevenueCatPackage {\n identifier: string;\n rcBillingProduct?: {\n identifier?: string;\n title?: string;\n description?: string;\n currentPrice?: {\n amountMicros?: number;\n formattedPrice?: string;\n };\n normalPeriodDuration?: string;\n defaultSubscriptionOption?: {\n introPrice?: {\n price?: {\n formattedPrice?: string;\n };\n periodDuration?: string;\n };\n trial?: {\n periodDuration?: string;\n };\n };\n };\n}\n\n/** RevenueCat CustomerInfo entitlement shape */\ninterface RevenueCatEntitlement {\n expirationDate?: number | null;\n latestPurchaseDate?: number | null;\n productIdentifier?: string;\n willRenew?: boolean;\n}\n\n/** RevenueCat CustomerInfo shape (simplified) */\ninterface RevenueCatCustomerInfo {\n entitlements: {\n active: Record<string, RevenueCatEntitlement>;\n };\n}\n\n/**\n * Convert RevenueCat package to our Product interface\n *\n * @param pkg - RevenueCat package\n * @returns Product object\n */\nexport function convertPackageToProduct(pkg: RevenueCatPackage): Product {\n const product = pkg.rcBillingProduct;\n const subscriptionOption = product?.defaultSubscriptionOption;\n\n const result: Product = {\n identifier: pkg.identifier,\n price: product?.currentPrice?.amountMicros\n ? (product.currentPrice.amountMicros / 1000000).toFixed(2)\n : '0',\n priceString: product?.currentPrice?.formattedPrice ?? '$0',\n title: product?.title ?? pkg.identifier,\n description: product?.description ?? '',\n };\n\n if (product?.identifier !== undefined) {\n result.productId = product.identifier;\n }\n if (product?.normalPeriodDuration !== undefined) {\n result.period = product.normalPeriodDuration;\n }\n if (subscriptionOption?.introPrice?.price?.formattedPrice !== undefined) {\n result.introPrice = subscriptionOption.introPrice.price.formattedPrice;\n }\n if (subscriptionOption?.introPrice?.periodDuration !== undefined) {\n result.introPricePeriod = subscriptionOption.introPrice.periodDuration;\n }\n if (subscriptionOption?.trial?.periodDuration !== undefined) {\n result.freeTrialPeriod = subscriptionOption.trial.periodDuration;\n }\n\n return result;\n}\n\n/**\n * Parse RevenueCat CustomerInfo into our Subscription interface\n *\n * @param customerInfo - RevenueCat customer info\n * @returns Subscription object\n */\nexport function parseCustomerInfo(\n customerInfo: RevenueCatCustomerInfo\n): Subscription {\n const hasActiveEntitlement =\n Object.keys(customerInfo.entitlements.active).length > 0;\n const activeEntitlement = Object.values(customerInfo.entitlements.active)[0];\n\n if (hasActiveEntitlement && activeEntitlement) {\n const subscription: Subscription = { isActive: true };\n\n if (activeEntitlement.expirationDate != null) {\n subscription.expirationDate = new Date(activeEntitlement.expirationDate);\n }\n if (activeEntitlement.latestPurchaseDate != null) {\n subscription.purchaseDate = new Date(\n activeEntitlement.latestPurchaseDate\n );\n }\n if (activeEntitlement.productIdentifier !== undefined) {\n subscription.productIdentifier = activeEntitlement.productIdentifier;\n }\n if (activeEntitlement.willRenew !== undefined) {\n subscription.willRenew = activeEntitlement.willRenew;\n }\n\n return subscription;\n }\n\n return { isActive: false };\n}\n\n/**\n * Get display name for subscription period\n *\n * @param period - ISO 8601 duration string (e.g., \"P1M\", \"P1Y\")\n * @returns Human-readable period name\n */\nexport function getPeriodDisplayName(period: string | undefined): string {\n if (!period) return '';\n\n const periodMap: Record<string, string> = {\n P1W: 'Weekly',\n P1M: 'Monthly',\n P3M: 'Quarterly',\n P6M: 'Semi-Annual',\n P1Y: 'Annual',\n P12M: 'Annual',\n };\n\n return periodMap[period] ?? period;\n}\n\n/**\n * Check if a plan is the \"best value\" (typically annual plans)\n *\n * @param period - ISO 8601 duration string\n * @returns Whether this is typically the best value option\n */\nexport function isBestValuePlan(period: string | undefined): boolean {\n return period === 'P1Y' || period === 'P12M';\n}\n\n/**\n * Get RevenueCat purchase error message\n *\n * @param errorCode - RevenueCat error code\n * @returns User-friendly error message\n */\nexport function getRevenueCatErrorMessage(errorCode: number): string {\n switch (errorCode) {\n case 1:\n return 'Purchase cancelled';\n case 2:\n return 'Store problem occurred. Please try again later.';\n case 3:\n return 'Purchase not allowed on this device';\n case 7:\n return 'Network error. Please check your connection.';\n default:\n return 'Purchase failed. Please try again.';\n }\n}\n\n// =============================================================================\n// Paywall display helpers\n// =============================================================================\n\n/** Named periods (as @sudobility/subscription_lib reports them) → ISO 8601. */\nconst NAMED_PERIOD_TO_ISO: Readonly<Record<string, string>> = {\n weekly: 'P1W',\n monthly: 'P1M',\n quarterly: 'P3M',\n yearly: 'P1Y',\n};\n\nfunction toIsoPeriod(period: string | null | undefined): string | undefined {\n if (!period) return undefined;\n return NAMED_PERIOD_TO_ISO[period.toLowerCase()] ?? period.toUpperCase();\n}\n\n/**\n * i18n key (in the `subscription` namespace) for a period's unit, or null.\n * `P1W` → `periods.week`, `P1M` → `periods.month`, `P3M` → `periods.quarter`,\n * `P6M` → `periods.halfYear`, `P1Y` / `P12M` → `periods.year`. Named periods\n * (`monthly`, `yearly`, ...) are accepted too.\n *\n * The web paywall's copy had no week and no `P12M` (it showed no unit for\n * them); both are mapped here.\n */\nexport function getPeriodLabelKey(\n period: string | null | undefined\n): string | null {\n switch (toIsoPeriod(period)) {\n case 'P1W':\n case 'P7D':\n return 'periods.week';\n case 'P1M':\n return 'periods.month';\n case 'P3M':\n return 'periods.quarter';\n case 'P6M':\n return 'periods.halfYear';\n case 'P1Y':\n case 'P12M':\n return 'periods.year';\n default:\n return null;\n }\n}\n\n/**\n * Price suffix for a period, e.g. `/month`, or '' for an unknown period.\n *\n * @param tSub - Translate function for the `subscription` namespace\n */\nexport function getPeriodLabel(\n tSub: TranslateFunction,\n period: string | null | undefined\n): string {\n const key = getPeriodLabelKey(period);\n return key ? `/${tSub(key)}` : '';\n}\n\n/**\n * Whether a product is the \"best value\" plan: an annual period (`P1Y`, `P12M`,\n * `yearly`) or an identifier containing `annual` / `yearly`\n * (case-insensitive). Unifies the web paywall's check with isBestValuePlan.\n */\nexport function isBestValueProduct(product: {\n period?: string | null;\n identifier?: string | null;\n}): boolean {\n const iso = toIsoPeriod(product.period);\n if (iso === 'P1Y' || iso === 'P12M') return true;\n const id = product.identifier?.toLowerCase() ?? '';\n return id.includes('annual') || id.includes('yearly');\n}\n\n/**\n * Length of a subscription period in months, or null when unknown.\n *\n * Accepts the named periods @sudobility/subscription_lib uses (`weekly` 1/4,\n * `monthly` 1, `quarterly` 3, `yearly` 12, `lifetime` Infinity, matching its\n * `periodToMonths`) and ISO 8601 durations (`P1W`, `P6M`, `P1Y`, `P30D`, ...).\n */\nexport function subscriptionPeriodToMonths(\n period: string | null | undefined\n): number | null {\n if (!period) return null;\n const lower = period.toLowerCase();\n switch (lower) {\n case 'weekly':\n return 1 / 4;\n case 'monthly':\n return 1;\n case 'quarterly':\n return 3;\n case 'yearly':\n return 12;\n case 'lifetime':\n return Infinity;\n }\n const match = /^P(\\d+)([DWMY])$/.exec(period.toUpperCase());\n if (!match?.[1] || !match[2]) return null;\n const value = parseInt(match[1], 10);\n switch (match[2]) {\n case 'D':\n return value / 30;\n case 'W':\n return value / 4;\n case 'M':\n return value;\n default:\n return value * 12;\n }\n}\n\n/** Price and period of a plan, for savings calculations. */\nexport interface PricedPeriod {\n /** Price in the store currency (major units) */\n price: number;\n /** Period, named (`monthly`) or ISO 8601 (`P1M`) */\n period?: string | null;\n}\n\n/**\n * Percent saved per month by `plan` compared with `base` (e.g. annual vs\n * monthly), rounded; null when not comparable (same or unknown/lifetime\n * period, missing product, zero base price) or when there is no saving.\n * Port of the RN SubscriptionScreen's calcSavingsPercent.\n */\nexport function calculateSavingsPercent(\n base: PricedPeriod | null | undefined,\n plan: PricedPeriod | null | undefined\n): number | null {\n if (!base || !plan) return null;\n const baseMonths = subscriptionPeriodToMonths(base.period);\n const planMonths = subscriptionPeriodToMonths(plan.period);\n if (baseMonths == null || planMonths == null) return null;\n if (baseMonths <= 0 || planMonths <= 0) return null;\n if (!isFinite(baseMonths) || !isFinite(planMonths)) return null;\n if (baseMonths === planMonths) return null;\n const baseMonthlyCost = base.price / baseMonths;\n const planMonthlyCost = plan.price / planMonths;\n if (baseMonthlyCost <= 0) return null;\n const savings = Math.round(\n ((baseMonthlyCost - planMonthlyCost) / baseMonthlyCost) * 100\n );\n return savings > 0 ? savings : null;\n}\n\n/**\n * Whether the active subscription was bought on another platform than this\n * client's (so it must be managed there). False when either side is unknown.\n * Matches the RN SubscriptionScreen's isCrossPlatform.\n */\nexport function isCrossPlatformSubscription(\n purchasePlatform: string | null | undefined,\n clientPlatform: string | null | undefined\n): boolean {\n return (\n !!purchasePlatform &&\n !!clientPlatform &&\n purchasePlatform !== clientPlatform\n );\n}\n\n/** Why an account cannot be deleted right now. */\nexport type DeleteAccountBlocker =\n /** No signed-in user */\n | 'not_signed_in'\n /** Anonymous (guest) user: there is no account to delete */\n | 'anonymous'\n /** An active subscription must be cancelled first */\n | 'active_subscription';\n\n/** Result of canDeleteAccount. */\nexport type DeleteAccountCheck =\n | { allowed: true }\n | { allowed: false; reason: DeleteAccountBlocker };\n\n/**\n * Whether the user may delete their account now. The rule both apps apply:\n * signed in with a real (non-anonymous) account, and no active subscription\n * (\"Please cancel your subscription before deleting your account.\" —\n * `deleteAccountUnsubscribeFirst`). The API also refuses deletion with an\n * active subscription when RevenueCat is configured.\n */\nexport function canDeleteAccount(state: {\n user:\n | { uid?: string | null; isAnonymous?: boolean | null }\n | null\n | undefined;\n subscriptionActive: boolean | null | undefined;\n}): DeleteAccountCheck {\n if (!state.user?.uid) return { allowed: false, reason: 'not_signed_in' };\n if (state.user.isAnonymous) return { allowed: false, reason: 'anonymous' };\n if (state.subscriptionActive) {\n return { allowed: false, reason: 'active_subscription' };\n }\n return { allowed: true };\n}\n\n/**\n * Fraction (0-1) of puzzles a subscription offer unlocks, summed from the\n * levels' `percentage`:\n *\n * - No offerId — only free levels (no subscription required)\n * - `'1_blue_belt'` — free levels + blue_belt levels\n * - Anything else — 1.0 (all puzzles)\n *\n * usePuzzleDistribution is the memoized hook form.\n */\nexport function getPuzzleDistribution(\n levels: ReadonlyArray<Pick<Level, 'entitlement' | 'percentage'>>,\n offerId?: string | null\n): number {\n if (offerId && offerId !== '1_blue_belt') return 1.0;\n\n let total = 0;\n for (const level of levels) {\n const levelOfferId = getSubscriptionOfferId(level.entitlement);\n if (\n !levelOfferId ||\n (offerId === '1_blue_belt' && levelOfferId === '1_blue_belt')\n ) {\n total += level.percentage ?? 0;\n }\n }\n return total;\n}\n\n/** A plan as the paywall lists it (price may be a string, as on Product). */\nexport interface PaywallPlan {\n /** Price in the store currency (major units), e.g. 4.99 or '4.99' */\n price: number | string;\n /** Period, named (`monthly`) or ISO 8601 (`P1M`) */\n period?: string | null;\n}\n\n/**\n * The plan the savings badge compares with: the one with the shortest known\n * period (usually monthly). Plans with an unknown period are skipped; the\n * first of equal periods wins. Null when no plan has a known period.\n * (Web SubscriptionPaywall's baseProduct.)\n */\nexport function selectSavingsBasePlan<T extends { period?: string | null }>(\n products: readonly T[]\n): T | null {\n let base: T | null = null;\n let baseMonths = Infinity;\n for (const product of products) {\n const months = subscriptionPeriodToMonths(product.period);\n if (months != null && months < baseMonths) {\n base = product;\n baseMonths = months;\n }\n }\n return base;\n}\n\n/**\n * Percent `plan` saves per month against the savings base plan of\n * `products` (selectSavingsBasePlan), via calculateSavingsPercent; null when\n * not comparable or no saving. The web paywall shows it only on the\n * best-value plan (isBestValueProduct).\n */\nexport function getPlanSavingsPercent(\n products: readonly PaywallPlan[],\n plan: PaywallPlan\n): number | null {\n const base = selectSavingsBasePlan(products);\n if (!base) return null;\n const toPriced = (p: PaywallPlan): PricedPeriod => ({\n price: typeof p.price === 'number' ? p.price : parseFloat(p.price),\n period: p.period ?? null,\n });\n return calculateSavingsPercent(toPriced(base), toPriced(plan));\n}\n"]}
|