forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcalculatorShare.ts
More file actions
117 lines (101 loc) · 3.16 KB
/
Copy pathcalculatorShare.ts
File metadata and controls
117 lines (101 loc) · 3.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import type { CalculatorState, CalculatorType, Participant, SplitItem } from '../components/SplitCalculator/SplitCalculator';
const SHARE_VERSION = 1;
export interface CalculatorSharePayload {
version: number;
state: CalculatorState;
}
export interface LegacyCalculatorSharePayload {
type: CalculatorType;
participants: Participant[];
items: SplitItem[];
subtotal: number;
currency: string;
rounding?: CalculatorState['rounding'];
}
function base64Encode(value: string): string {
const bytes = new TextEncoder().encode(value);
let binary = '';
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return btoa(binary);
}
function base64Decode(value: string): string {
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
function isCalculatorState(value: unknown): value is CalculatorState {
if (typeof value !== 'object' || value === null) return false;
const candidate = value as CalculatorState;
return (
candidate != null &&
typeof candidate.type === 'string' &&
Array.isArray(candidate.participants) &&
Array.isArray(candidate.items) &&
typeof candidate.totalAmount === 'number' &&
typeof candidate.taxAmount === 'number' &&
typeof candidate.tipAmount === 'number' &&
typeof candidate.currency === 'string' &&
['none', 'up', 'down', 'nearest'].includes(candidate.rounding)
);
}
export function encodeCalculatorShare(state: CalculatorState): string {
const payload: CalculatorSharePayload = {
version: SHARE_VERSION,
state,
};
return base64Encode(JSON.stringify(payload));
}
function parseLegacyPayload(value: unknown): CalculatorState | null {
if (typeof value !== 'object' || value === null) {
return null;
}
const legacy = value as LegacyCalculatorSharePayload;
if (
typeof legacy.type !== 'string' ||
!Array.isArray(legacy.participants) ||
!Array.isArray(legacy.items) ||
typeof legacy.subtotal !== 'number' ||
typeof legacy.currency !== 'string'
) {
return null;
}
return {
type: legacy.type,
participants: legacy.participants,
items: legacy.items,
totalAmount: legacy.subtotal,
taxAmount: 0,
tipAmount: 0,
currency: legacy.currency,
rounding: legacy.rounding ?? 'none',
};
}
export function decodeCalculatorShare(encoded: string): CalculatorState | null {
try {
const raw = decodeURIComponent(encoded);
const json = base64Decode(raw);
const parsed = JSON.parse(json) as unknown;
if (typeof parsed === 'object' && parsed !== null && 'version' in parsed) {
const payload = parsed as CalculatorSharePayload;
if (payload.version !== SHARE_VERSION) {
return null;
}
if (isCalculatorState(payload.state)) {
return payload.state;
}
return null;
}
return parseLegacyPayload(parsed);
} catch {
return null;
}
}
export function buildCalculatorShareUrl(state: CalculatorState): string {
const encoded = encodeURIComponent(encodeCalculatorShare(state));
return `/calculator?data=${encoded}`;
}