@monimesl/dot-cli 1.0.0-beta.1 → 1.0.0-beta.11
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/README.md +22 -9
- package/dist/cli.js +15 -12
- package/dist/cli.js.map +1 -1
- package/dist/{server-DMnWDdJ6.js → server-BhnO9_Zu.js} +56 -7
- package/dist/server-BhnO9_Zu.js.map +1 -0
- package/dist/templates/showcase/.env +2 -2
- package/dist/templates/showcase/dot.ts +22 -2
- package/dist/templates/showcase/pages/basics/bars/controller.ts +2 -2
- package/dist/templates/showcase/pages/basics/bars/view.dml +2 -1
- package/dist/templates/showcase/pages/basics/bindings/state.ts +2 -2
- package/dist/templates/showcase/pages/basics/bindings/view.dml +2 -2
- package/dist/templates/showcase/pages/basics/colors/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/concert/view.dml +1 -1
- package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/control-flow/view.dml +1 -1
- package/dist/templates/showcase/pages/basics/events/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/events/view.dml +1 -1
- package/dist/templates/showcase/pages/basics/hooks/controller.ts +2 -1
- package/dist/templates/showcase/pages/basics/hooks/view.dml +2 -2
- package/dist/templates/showcase/pages/basics/navigation/controller.ts +1 -0
- package/dist/templates/showcase/pages/basics/navigation/view.dml +1 -1
- package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +3 -3
- package/dist/templates/showcase/pages/basics/share/view.dml +2 -2
- package/dist/templates/showcase/pages/basics/state/controller.ts +9 -0
- package/dist/templates/showcase/pages/basics/state/state.ts +3 -0
- package/dist/templates/showcase/pages/basics/theme/controller.ts +28 -0
- package/dist/templates/showcase/pages/basics/theme/state.ts +4 -0
- package/dist/templates/showcase/pages/basics/theme/view.dml +50 -0
- package/dist/templates/showcase/pages/components/capture/controller.ts +25 -0
- package/dist/templates/showcase/pages/components/capture/state.ts +7 -0
- package/dist/templates/showcase/pages/components/capture/view.dml +32 -0
- package/dist/templates/showcase/pages/components/carousel/view.dml +2 -2
- package/dist/templates/showcase/pages/components/feedback/controller.ts +2 -1
- package/dist/templates/showcase/pages/components/feedback/state.ts +2 -2
- package/dist/templates/showcase/pages/components/feedback/view.dml +2 -2
- package/dist/templates/showcase/pages/components/forms/controller.ts +2 -2
- package/dist/templates/showcase/pages/components/forms/state.ts +1 -1
- package/dist/templates/showcase/pages/components/forms/view.dml +6 -5
- package/dist/templates/showcase/pages/components/icons/controller.ts +1 -0
- package/dist/templates/showcase/pages/components/icons/state.ts +1 -1
- package/dist/templates/showcase/pages/components/images/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/images/state.ts +1 -1
- package/dist/templates/showcase/pages/components/images/view.dml +4 -4
- package/dist/templates/showcase/pages/components/layout/controller.ts +5 -1
- package/dist/templates/showcase/pages/components/layout/state.ts +2 -0
- package/dist/templates/showcase/pages/components/layout/view.dml +44 -6
- package/dist/templates/showcase/pages/components/lists/view.dml +12 -7
- package/dist/templates/showcase/pages/components/money/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/money/view.dml +2 -2
- package/dist/templates/showcase/pages/components/pinned/controller.ts +10 -0
- package/dist/templates/showcase/pages/components/pinned/state.ts +25 -0
- package/dist/templates/showcase/pages/components/pinned/view.dml +42 -0
- package/dist/templates/showcase/pages/components/reading/state.ts +2 -2
- package/dist/templates/showcase/pages/components/sheets/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/sheets/view.dml +1 -1
- package/dist/templates/showcase/pages/components/styles/view.dml +39 -4
- package/dist/templates/showcase/pages/components/tabs/state.ts +2 -2
- package/dist/templates/showcase/pages/components/tabs/view.dml +6 -5
- package/dist/templates/showcase/pages/components/text/view.dml +4 -4
- package/dist/templates/showcase/pages/dash/account/controller.ts +3 -0
- package/dist/templates/showcase/pages/dash/account/view.dml +2 -2
- package/dist/templates/showcase/pages/dash/cache/controller.ts +45 -16
- package/dist/templates/showcase/pages/dash/cache/state.ts +1 -0
- package/dist/templates/showcase/pages/dash/cache/view.dml +4 -0
- package/dist/templates/showcase/pages/dash/calls/controller.ts +3 -2
- package/dist/templates/showcase/pages/dash/contacts/controller.ts +1 -0
- package/dist/templates/showcase/pages/dash/crypto/controller.ts +2 -0
- package/dist/templates/showcase/pages/dash/crypto/view.dml +1 -1
- package/dist/templates/showcase/pages/dash/files/controller.ts +67 -32
- package/dist/templates/showcase/pages/dash/files/state.ts +1 -0
- package/dist/templates/showcase/pages/dash/files/view.dml +5 -1
- package/dist/templates/showcase/pages/dash/media/controller.ts +7 -1
- package/dist/templates/showcase/pages/dash/media/state.ts +2 -0
- package/dist/templates/showcase/pages/dash/network/controller.ts +5 -1
- package/dist/templates/showcase/pages/dash/network/view.dml +1 -1
- package/dist/templates/showcase/pages/dash/payments/cases.ts +40 -22
- package/dist/templates/showcase/pages/dash/payments/controller.ts +27 -3
- package/dist/templates/showcase/pages/dash/payments/state.ts +2 -0
- package/dist/templates/showcase/pages/dash/payments/view.dml +3 -1
- package/dist/templates/showcase/pages/dash/reminders/controller.ts +73 -0
- package/dist/templates/showcase/pages/dash/reminders/state.ts +6 -0
- package/dist/templates/showcase/pages/dash/reminders/view.dml +34 -0
- package/dist/templates/showcase/pages/dash/secrets/controller.ts +37 -14
- package/dist/templates/showcase/pages/dash/secrets/state.ts +1 -0
- package/dist/templates/showcase/pages/dash/secrets/view.dml +4 -0
- package/dist/templates/showcase/pages/dash/system/controller.ts +5 -0
- package/dist/templates/showcase/pages/dash/system/view.dml +4 -4
- package/dist/templates/showcase/pages/dash/ui/controller.ts +6 -1
- package/dist/templates/showcase/pages/index/controller.ts +1 -0
- package/dist/templates/showcase/pages/index/state.ts +30 -6
- package/dist/templates/showcase/statics/icons/ticket-stub.svg +1 -0
- package/dist/templates/starter/.env +2 -2
- package/dist/templates/starter/dot.ts +1 -0
- package/dist/templates/starter/pages/index/controller.ts +2 -0
- package/dist/templates/starter/pages/index/state.ts +2 -0
- package/dist/templates/starter/pages/index/view.dml +2 -1
- package/package.json +6 -6
- package/dist/server-DMnWDdJ6.js.map +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { errorMessage } from '@monimesl/dot-runtime';
|
|
2
|
+
|
|
1
3
|
import state from './state';
|
|
2
4
|
|
|
3
5
|
const RECENT = 'recent/';
|
|
@@ -12,38 +14,65 @@ export default definePage({
|
|
|
12
14
|
await this.actions.refresh();
|
|
13
15
|
},
|
|
14
16
|
},
|
|
17
|
+
// Values are JSON: text, numbers, true or false, lists and objects. Each can be up to 1 MB,
|
|
18
|
+
// and the cache up to 64 MB. A full cache rejects with 'failed'.
|
|
15
19
|
actions: {
|
|
16
20
|
async refresh() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
try {
|
|
22
|
+
const keys = await this.dash.storage.cache.keys();
|
|
23
|
+
const recent: string[] = [];
|
|
24
|
+
for (const key of keys.filter((each) => each.startsWith(RECENT))) {
|
|
25
|
+
// get gives null for a key that's missing or expired.
|
|
26
|
+
recent.push((await this.dash.storage.cache.get<string>(key)) ?? '');
|
|
27
|
+
}
|
|
28
|
+
const hasGenre = await this.dash.storage.cache.exists(GENRE);
|
|
29
|
+
this.setState({
|
|
30
|
+
recent,
|
|
31
|
+
genre: hasGenre ? ((await this.dash.storage.cache.get<string>(GENRE)) ?? '') : '',
|
|
32
|
+
keys: keys.join(', '),
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
this.setState({ note: errorMessage(error) });
|
|
21
36
|
}
|
|
22
|
-
const hasGenre = await this.dash.storage.cache.exists(GENRE);
|
|
23
|
-
this.setState({
|
|
24
|
-
recent,
|
|
25
|
-
genre: hasGenre ? ((await this.dash.storage.cache.get<string>(GENRE)) ?? '') : '',
|
|
26
|
-
keys: keys.join(', '),
|
|
27
|
-
});
|
|
28
37
|
},
|
|
29
38
|
// Each view lasts 30 seconds, then the cache forgets it.
|
|
30
39
|
async view(_state, event: DataEvent<{ id: string; name: string }>) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
try {
|
|
41
|
+
await this.dash.storage.cache.set(`${RECENT}${event.data.id}`, event.data.name, {
|
|
42
|
+
ttl: RECENT_MILLISECONDS,
|
|
43
|
+
});
|
|
44
|
+
this.setState({ note: '' });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.setState({ note: errorMessage(error) });
|
|
47
|
+
}
|
|
34
48
|
await this.actions.refresh();
|
|
35
49
|
},
|
|
36
50
|
// With no ttl, the genre lasts until it's removed.
|
|
37
51
|
async setGenre(_state, event: ValueEvent) {
|
|
38
|
-
|
|
52
|
+
try {
|
|
53
|
+
await this.dash.storage.cache.set(GENRE, event.detail.value);
|
|
54
|
+
this.setState({ note: '' });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this.setState({ note: errorMessage(error) });
|
|
57
|
+
}
|
|
39
58
|
await this.actions.refresh();
|
|
40
59
|
},
|
|
41
60
|
async forgetGenre() {
|
|
42
|
-
|
|
61
|
+
try {
|
|
62
|
+
await this.dash.storage.cache.remove(GENRE);
|
|
63
|
+
this.setState({ note: '' });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.setState({ note: errorMessage(error) });
|
|
66
|
+
}
|
|
43
67
|
await this.actions.refresh();
|
|
44
68
|
},
|
|
45
69
|
async clear() {
|
|
46
|
-
|
|
70
|
+
try {
|
|
71
|
+
await this.dash.storage.cache.clear();
|
|
72
|
+
this.setState({ note: '' });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.setState({ note: errorMessage(error) });
|
|
75
|
+
}
|
|
47
76
|
await this.actions.refresh();
|
|
48
77
|
},
|
|
49
78
|
},
|
|
@@ -3,7 +3,7 @@ import { errorMessage, isDashError } from '@monimesl/dot-runtime';
|
|
|
3
3
|
|
|
4
4
|
import state from './state';
|
|
5
5
|
|
|
6
|
-
// The long wait, kept so the Cancel button can stop it.
|
|
6
|
+
// The long wait, kept so the Cancel button can stop it. cancel() makes it reject with 'cancelled'.
|
|
7
7
|
let wait: DashRequest<void> | null = null;
|
|
8
8
|
|
|
9
9
|
export default definePage({
|
|
@@ -27,7 +27,8 @@ export default definePage({
|
|
|
27
27
|
try {
|
|
28
28
|
switch (event.data.call) {
|
|
29
29
|
case 'timeout':
|
|
30
|
-
//
|
|
30
|
+
// Every call takes { timeout } last, except payments.request, where Dash decides.
|
|
31
|
+
// This gives up after 1 second on a wait of 5.
|
|
31
32
|
await this.dash.system.delay(5000, { timeout: 1000 });
|
|
32
33
|
break;
|
|
33
34
|
case 'cancelled':
|
|
@@ -6,6 +6,7 @@ export default definePage({
|
|
|
6
6
|
initialState: state,
|
|
7
7
|
configs: { navigationBar: { title: 'Contacts' } },
|
|
8
8
|
actions: {
|
|
9
|
+
// Resolves with { name, phone }, the number as the contact has it, such as "076 123456".
|
|
9
10
|
async choose() {
|
|
10
11
|
try {
|
|
11
12
|
const contact = await unlessCancelled(this.dash.contacts.pickPhone());
|
|
@@ -12,6 +12,7 @@ export default definePage({
|
|
|
12
12
|
encode: (state) => ({ encoded: dash.utils.base64.encode(state.text) }),
|
|
13
13
|
},
|
|
14
14
|
actions: {
|
|
15
|
+
// Text goes in as UTF-8. Binary data goes in as { base64: '…' }.
|
|
15
16
|
async hash(state) {
|
|
16
17
|
try {
|
|
17
18
|
const sha256 = await this.dash.crypto.sha256(state.text);
|
|
@@ -25,6 +26,7 @@ export default definePage({
|
|
|
25
26
|
this.setState({ note: errorMessage(error) });
|
|
26
27
|
}
|
|
27
28
|
},
|
|
29
|
+
// randomBytes takes a count from 1 to 1024.
|
|
28
30
|
async makeRandom() {
|
|
29
31
|
try {
|
|
30
32
|
const random = await this.dash.crypto.randomBytes(16);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">dash.crypto</Text>
|
|
3
|
-
<Text>Hashes, signatures and random values, worked out on the phone.
|
|
3
|
+
<Text>Hashes, signatures and random values, worked out on the phone. They come as hex, or as base64 with { as: 'base64' }. randomUUID gives a UUID.</Text>
|
|
4
4
|
|
|
5
5
|
<View variant="card" gap="medium">
|
|
6
6
|
<Input label="Text" value="{{ text }}" onChange="setText" />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { errorMessage } from '@monimesl/dot-runtime';
|
|
2
|
+
|
|
1
3
|
import type { Archive, Song } from './state';
|
|
2
4
|
import state from './state';
|
|
3
5
|
|
|
@@ -14,57 +16,90 @@ export default definePage({
|
|
|
14
16
|
mutations: {
|
|
15
17
|
setDraft: (_state, event: ValueEvent) => ({ draft: event.detail.value }),
|
|
16
18
|
},
|
|
19
|
+
// Paths are inside the dot's folder, and '' is the folder itself. A call on a path with nothing
|
|
20
|
+
// there rejects with 'missing', while stat gives null.
|
|
17
21
|
actions: {
|
|
18
22
|
async refresh() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
try {
|
|
24
|
+
const entries = await this.dash.storage.file.list('', { recursive: true });
|
|
25
|
+
const songs: Song[] = [];
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (entry.kind === 'file' && entry.path.startsWith('setlist/')) {
|
|
28
|
+
songs.push({ path: entry.path, text: await this.dash.storage.file.read(entry.path) });
|
|
29
|
+
}
|
|
24
30
|
}
|
|
31
|
+
const archives: Archive[] = entries
|
|
32
|
+
.filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
|
|
33
|
+
.map((entry) => ({
|
|
34
|
+
path: entry.path,
|
|
35
|
+
time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
|
|
36
|
+
}))
|
|
37
|
+
.reverse();
|
|
38
|
+
const documents = entries.filter(
|
|
39
|
+
(entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
|
|
40
|
+
);
|
|
41
|
+
// In bytes: what the dot's files take, and the most they may take.
|
|
42
|
+
const { used, quota } = await this.dash.storage.file.usage();
|
|
43
|
+
const kept = await this.dash.storage.file.stat(PICTURE);
|
|
44
|
+
this.setState({
|
|
45
|
+
songs,
|
|
46
|
+
archives,
|
|
47
|
+
documents,
|
|
48
|
+
usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
|
|
49
|
+
picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.setState({ note: errorMessage(error) });
|
|
25
53
|
}
|
|
26
|
-
const archives: Archive[] = entries
|
|
27
|
-
.filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
|
|
28
|
-
.map((entry) => ({
|
|
29
|
-
path: entry.path,
|
|
30
|
-
time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
|
|
31
|
-
}))
|
|
32
|
-
.reverse();
|
|
33
|
-
const documents = entries.filter(
|
|
34
|
-
(entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
|
|
35
|
-
);
|
|
36
|
-
const { used, quota } = await this.dash.storage.file.usage();
|
|
37
|
-
const kept = await this.dash.storage.file.stat(PICTURE);
|
|
38
|
-
this.setState({
|
|
39
|
-
songs,
|
|
40
|
-
archives,
|
|
41
|
-
documents,
|
|
42
|
-
usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
|
|
43
|
-
picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
|
|
44
|
-
});
|
|
45
54
|
},
|
|
46
55
|
async save(state) {
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
try {
|
|
57
|
+
await this.dash.storage.file.write(`setlist/${Date.now()}.txt`, state.draft);
|
|
58
|
+
this.setState({ draft: '', note: '' });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.setState({ note: errorMessage(error) });
|
|
61
|
+
}
|
|
49
62
|
await this.actions.refresh();
|
|
50
63
|
},
|
|
51
64
|
// Moves the whole setlist folder into the archive, under the time it was archived.
|
|
52
65
|
async archive() {
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
try {
|
|
67
|
+
await this.dash.storage.file.createFolder('archive');
|
|
68
|
+
await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
|
|
69
|
+
this.setState({ note: '' });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.setState({ note: errorMessage(error) });
|
|
72
|
+
}
|
|
55
73
|
await this.actions.refresh();
|
|
56
74
|
},
|
|
57
|
-
// Copies an archived setlist back, in place of the current one.
|
|
75
|
+
// Copies an archived setlist back, in place of the current one. Without overwrite, a path
|
|
76
|
+
// that's taken rejects with 'failed'.
|
|
58
77
|
async copyBack(_state, event: DataEvent<{ path: string }>) {
|
|
59
|
-
|
|
78
|
+
try {
|
|
79
|
+
await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
|
|
80
|
+
this.setState({ note: '' });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.setState({ note: errorMessage(error) });
|
|
83
|
+
}
|
|
60
84
|
await this.actions.refresh();
|
|
61
85
|
},
|
|
86
|
+
// A folder with files in it needs recursive.
|
|
62
87
|
async clearSetlist() {
|
|
63
|
-
|
|
88
|
+
try {
|
|
89
|
+
await this.dash.storage.file.remove('setlist', { recursive: true });
|
|
90
|
+
this.setState({ note: '' });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.setState({ note: errorMessage(error) });
|
|
93
|
+
}
|
|
64
94
|
await this.actions.refresh();
|
|
65
95
|
},
|
|
66
96
|
async forgetPicture() {
|
|
67
|
-
|
|
97
|
+
try {
|
|
98
|
+
await this.dash.storage.file.remove(PICTURE);
|
|
99
|
+
this.setState({ note: '' });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.setState({ note: errorMessage(error) });
|
|
102
|
+
}
|
|
68
103
|
await this.actions.refresh();
|
|
69
104
|
},
|
|
70
105
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">dash.storage.file</Text>
|
|
3
|
-
<Text>Each dot has its own folder on the phone. Other dots can't see it, and its files stay after the dot closes.</Text>
|
|
3
|
+
<Text>Each dot has its own folder on the phone. Other dots can't see it, and its files stay after the dot closes. Like the cache and secrets, it isn't backed up, so keep what matters on the dot's server too.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">write, read and list</Text>
|
|
@@ -75,4 +75,8 @@
|
|
|
75
75
|
<Text variant="subtitle">usage</Text>
|
|
76
76
|
<Text>{{ usage }}</Text>
|
|
77
77
|
</View>
|
|
78
|
+
|
|
79
|
+
<If condition="{{ note }}">
|
|
80
|
+
<Notice tone="danger">{{ note }}</Notice>
|
|
81
|
+
</If>
|
|
78
82
|
</View>
|
|
@@ -6,8 +6,12 @@ import state from './state';
|
|
|
6
6
|
export default definePage({
|
|
7
7
|
initialState: state,
|
|
8
8
|
configs: { navigationBar: { title: 'Photos, files and scanning' } },
|
|
9
|
-
//
|
|
9
|
+
// Each call here needs a permission in dot.json. Dash asks the person the first time. If dot.json
|
|
10
|
+
// doesn't list it, or the person says no, the call rejects with 'denied'. unlessCancelled gives
|
|
11
|
+
// undefined when the person closes the picker or scanner.
|
|
10
12
|
actions: {
|
|
13
|
+
// Resolves with a DotFile: a JPEG of at most 2048 pixels on its longest side, without its
|
|
14
|
+
// location. A phone without a camera rejects with 'unsupported'.
|
|
11
15
|
async pickPhoto(_state, event: DataEvent<{ source: PhotoSource }>) {
|
|
12
16
|
try {
|
|
13
17
|
const photo = await unlessCancelled(
|
|
@@ -22,6 +26,7 @@ export default definePage({
|
|
|
22
26
|
this.setState({ note: errorMessage(error) });
|
|
23
27
|
}
|
|
24
28
|
},
|
|
29
|
+
// A file larger than 25 MB rejects with 'invalid'.
|
|
25
30
|
async pickTicket() {
|
|
26
31
|
try {
|
|
27
32
|
const ticket = await unlessCancelled(
|
|
@@ -51,6 +56,7 @@ export default definePage({
|
|
|
51
56
|
this.setState({ note: errorMessage(error) });
|
|
52
57
|
}
|
|
53
58
|
},
|
|
59
|
+
// Resolves with { text, format }. Only QR codes for now.
|
|
54
60
|
async scanTicket() {
|
|
55
61
|
try {
|
|
56
62
|
const scanned = await unlessCancelled(this.dash.media.scanCode());
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DotFile } from '@monimesl/dot-runtime';
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
|
+
// A DotFile has a name, a size in bytes, a contentType and a url to show it with. It's temporary,
|
|
5
|
+
// so it goes when the dot closes.
|
|
4
6
|
photo: null as DotFile | null,
|
|
5
7
|
photoDetails: '',
|
|
6
8
|
ticket: null as DotFile | null,
|
|
@@ -87,6 +87,8 @@ export default definePage({
|
|
|
87
87
|
this.setState({ connection: errorMessage(error) });
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
+
// Like fetch, it resolves with any answer, even a 404, so check ok. It rejects only when no
|
|
91
|
+
// answer comes, and with 'denied' for a host that network.hosts in dot.json doesn't list.
|
|
90
92
|
async searchAsText(state) {
|
|
91
93
|
try {
|
|
92
94
|
const response = await this.dash.network.http.request({
|
|
@@ -113,6 +115,7 @@ export default definePage({
|
|
|
113
115
|
this.setState({ limit: state.limit + PAGE });
|
|
114
116
|
await this.actions.load();
|
|
115
117
|
},
|
|
118
|
+
// With as: 'json', an answer that isn't JSON rejects with 'failed'.
|
|
116
119
|
async load(state) {
|
|
117
120
|
this.setState({ searching: true, note: '' });
|
|
118
121
|
try {
|
|
@@ -153,7 +156,8 @@ export default definePage({
|
|
|
153
156
|
return;
|
|
154
157
|
}
|
|
155
158
|
try {
|
|
156
|
-
//
|
|
159
|
+
// Resolves with ok and a temporary file for a 2xx answer of up to 256 MB. The catalog's
|
|
160
|
+
// artwork comes in any size its address names.
|
|
157
161
|
const response = await this.dash.network.http.download({
|
|
158
162
|
url: release.artwork.replace('100x100bb', '300x300bb'),
|
|
159
163
|
name: 'artwork.jpg',
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
|
|
98
98
|
<View gap="small">
|
|
99
99
|
<Text variant="subtitle">Hosts dot.json doesn't list</Text>
|
|
100
|
-
<Text variant="caption" tone="muted">Dash turns the request down.</Text>
|
|
100
|
+
<Text variant="caption" tone="muted">Dash turns the request down, with the code denied.</Text>
|
|
101
101
|
<View variant="card" gap="small">
|
|
102
102
|
<Button variant="secondary" onClick="tryUnlistedHost">Reach example.com</Button>
|
|
103
103
|
<If condition="{{ note }}">
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
LinkProvider,
|
|
3
|
+
MomoProvider,
|
|
4
|
+
OrderLineItem,
|
|
5
|
+
PaymentMethod,
|
|
6
|
+
PaymentOptions,
|
|
7
|
+
} from '@monimesl/dot-runtime';
|
|
2
8
|
|
|
3
9
|
// What the tester sets above the cases.
|
|
4
10
|
export interface CaseInput {
|
|
@@ -17,6 +23,8 @@ export interface PaymentCase {
|
|
|
17
23
|
reusesReference?: boolean;
|
|
18
24
|
// Calls cancel() this long after asking.
|
|
19
25
|
cancelAfterMilliseconds?: number;
|
|
26
|
+
// Asks a second time while the sheet is open, as a double tap would.
|
|
27
|
+
asksTwice?: boolean;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
export interface CaseGroup {
|
|
@@ -26,8 +34,8 @@ export interface CaseGroup {
|
|
|
26
34
|
cases: PaymentCase[];
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
// SLE 0.50 a ticket, in minor units as Monime counts money
|
|
30
|
-
// little: the two tickets the page starts with come to SLE 1.00.
|
|
37
|
+
// SLE 0.50 a ticket, in minor units as Monime counts money. In Dash the money is real, so the
|
|
38
|
+
// cases cost little: the two tickets the page starts with come to SLE 1.00.
|
|
31
39
|
const TICKET_PRICE = 50;
|
|
32
40
|
const SAMPLE_PHONE_NUMBER = '+23276123456';
|
|
33
41
|
const SAMPLE_ACCOUNT_ID = 'fac-k6VgVTfSmLruHpiQgMg4DUeySZx';
|
|
@@ -115,13 +123,13 @@ export const GROUPS: CaseGroup[] = [
|
|
|
115
123
|
id: 'orange',
|
|
116
124
|
title: 'Orange Money only',
|
|
117
125
|
expect: 'Mobile money with only Orange Money (m17).',
|
|
118
|
-
options: paying([{ type: 'momo', providers:
|
|
126
|
+
options: paying([{ type: 'momo', providers: ['m17'] }]),
|
|
119
127
|
},
|
|
120
128
|
{
|
|
121
129
|
id: 'africell',
|
|
122
130
|
title: 'Africell Money only',
|
|
123
131
|
expect: 'Mobile money with only Africell Money (m18).',
|
|
124
|
-
options: paying([{ type: 'momo', providers:
|
|
132
|
+
options: paying([{ type: 'momo', providers: ['m18'] }]),
|
|
125
133
|
},
|
|
126
134
|
{
|
|
127
135
|
id: 'one-number',
|
|
@@ -134,30 +142,30 @@ export const GROUPS: CaseGroup[] = [
|
|
|
134
142
|
),
|
|
135
143
|
},
|
|
136
144
|
{
|
|
137
|
-
id: '
|
|
138
|
-
title: '
|
|
139
|
-
expect: 'Linked accounts,
|
|
140
|
-
options: paying([{ type: 'link', providers:
|
|
145
|
+
id: 'any-provider',
|
|
146
|
+
title: 'An empty list of providers',
|
|
147
|
+
expect: 'Linked accounts at any bank, as an empty list means all of them.',
|
|
148
|
+
options: paying([{ type: 'link', providers: [] }]),
|
|
141
149
|
},
|
|
142
150
|
{
|
|
143
|
-
id: 'banks',
|
|
144
|
-
title: '
|
|
145
|
-
expect: 'Linked accounts at slb001
|
|
146
|
-
options: paying([{ type: 'link', providers:
|
|
151
|
+
id: 'two-banks',
|
|
152
|
+
title: 'Accounts at two banks',
|
|
153
|
+
expect: 'Linked accounts at slb001 and slb009 only.',
|
|
154
|
+
options: paying([{ type: 'link', providers: ['slb001', 'slb009'] }]),
|
|
147
155
|
},
|
|
148
156
|
{
|
|
149
157
|
id: 'orange-or-account',
|
|
150
158
|
title: 'Orange Money from one number, or an account',
|
|
151
159
|
expect:
|
|
152
|
-
'Mobile money limited to Orange Money and the number above, then linked accounts
|
|
160
|
+
'Mobile money limited to Orange Money and the number above, then linked accounts at slb001 only.',
|
|
153
161
|
options: (input, reference) =>
|
|
154
162
|
paying([
|
|
155
163
|
{
|
|
156
164
|
type: 'momo',
|
|
157
|
-
providers:
|
|
165
|
+
providers: ['m17'],
|
|
158
166
|
phoneNumber: input.phoneNumber || SAMPLE_PHONE_NUMBER,
|
|
159
167
|
},
|
|
160
|
-
{ type: 'link', providers:
|
|
168
|
+
{ type: 'link', providers: ['slb001'] },
|
|
161
169
|
])(input, reference),
|
|
162
170
|
},
|
|
163
171
|
],
|
|
@@ -261,6 +269,14 @@ export const GROUPS: CaseGroup[] = [
|
|
|
261
269
|
options: (input, reference) => jazzNight(reference, [tickets(input)]),
|
|
262
270
|
cancelAfterMilliseconds: 20_000,
|
|
263
271
|
},
|
|
272
|
+
{
|
|
273
|
+
id: 'two-at-once',
|
|
274
|
+
title: 'Two requests at once',
|
|
275
|
+
expect:
|
|
276
|
+
'One sheet opens, and it carries on. The second request rejects as denied, shown behind the sheet.',
|
|
277
|
+
options: (input, reference) => jazzNight(reference, [tickets(input)]),
|
|
278
|
+
asksTwice: true,
|
|
279
|
+
},
|
|
264
280
|
],
|
|
265
281
|
},
|
|
266
282
|
{
|
|
@@ -299,7 +315,7 @@ export const GROUPS: CaseGroup[] = [
|
|
|
299
315
|
id: 'invalid',
|
|
300
316
|
title: 'Refused before Dash',
|
|
301
317
|
subtitle:
|
|
302
|
-
'Every host checks these the same way
|
|
318
|
+
'Every host checks these the same way. Each rejects as invalid, with a message that starts as shown.',
|
|
303
319
|
cases: [
|
|
304
320
|
{
|
|
305
321
|
id: 'no-items',
|
|
@@ -399,16 +415,18 @@ export const GROUPS: CaseGroup[] = [
|
|
|
399
415
|
options: paying([{ type: 'card' }, { type: 'card' }]),
|
|
400
416
|
},
|
|
401
417
|
{
|
|
402
|
-
id: '
|
|
403
|
-
title: '
|
|
404
|
-
expect:
|
|
405
|
-
|
|
418
|
+
id: 'providers-not-a-list',
|
|
419
|
+
title: 'Providers that are not a list',
|
|
420
|
+
expect: 'invalid: link\'s providers must be a list of provider IDs, such as ["slb001"]',
|
|
421
|
+
// The types catch this. The cast gets past them, to show what Dash tells a dot in plain JavaScript.
|
|
422
|
+
options: paying([{ type: 'link', providers: 'slb001' as unknown as LinkProvider[] }]),
|
|
406
423
|
},
|
|
407
424
|
{
|
|
408
425
|
id: 'unknown-provider',
|
|
409
426
|
title: "A provider Monime doesn't know",
|
|
410
427
|
expect: 'invalid: "m99" isn\'t a provider Monime knows for momo',
|
|
411
|
-
|
|
428
|
+
// The types catch this too.
|
|
429
|
+
options: paying([{ type: 'momo', providers: ['m99' as unknown as MomoProvider] }]),
|
|
412
430
|
},
|
|
413
431
|
{
|
|
414
432
|
id: 'local-number',
|
|
@@ -24,6 +24,7 @@ export default definePage({
|
|
|
24
24
|
setFinancialAccountId: (_state, event: ValueEvent) => ({
|
|
25
25
|
financialAccountId: event.detail.value.trim(),
|
|
26
26
|
}),
|
|
27
|
+
clearError: () => ({ error: '' }),
|
|
27
28
|
},
|
|
28
29
|
actions: {
|
|
29
30
|
// data-case names the case to run, from cases.ts.
|
|
@@ -51,7 +52,16 @@ export default definePage({
|
|
|
51
52
|
phoneNumber: state.phoneNumber,
|
|
52
53
|
financialAccountId: state.financialAccountId,
|
|
53
54
|
};
|
|
55
|
+
// Once the person picks a method, it resolves with how the payment ended: completed,
|
|
56
|
+
// pending, expired or cancelled. Before that, it rejects with 'cancelled' if the sheet
|
|
57
|
+
// closes, and with 'invalid' or 'denied' if Monime won't take the order.
|
|
54
58
|
const request = this.dash.payments.request(paymentCase.options(input, reference));
|
|
59
|
+
if (paymentCase.asksTwice) {
|
|
60
|
+
// Only one sheet opens at a time, so this rejects as denied and the first carries on.
|
|
61
|
+
this.dash.payments
|
|
62
|
+
.request(paymentCase.options(input, `${reference}-again`))
|
|
63
|
+
.catch((error: unknown) => this.setState({ error: errorOutcome(error) }));
|
|
64
|
+
}
|
|
55
65
|
const cancelAfter = paymentCase.cancelAfterMilliseconds;
|
|
56
66
|
if (cancelAfter !== undefined) {
|
|
57
67
|
// Runs beside the payment. Cancelling one that already ended does nothing.
|
|
@@ -67,6 +77,7 @@ export default definePage({
|
|
|
67
77
|
this.setState(shownResult(result));
|
|
68
78
|
} catch (error) {
|
|
69
79
|
outcome = errorOutcome(error);
|
|
80
|
+
this.setState({ error: outcome });
|
|
70
81
|
}
|
|
71
82
|
// Another run may have started meanwhile, so this one names itself again.
|
|
72
83
|
this.setState((current) => ({
|
|
@@ -74,9 +85,13 @@ export default definePage({
|
|
|
74
85
|
runs: added(current.runs, paymentCase.title, outcome),
|
|
75
86
|
}));
|
|
76
87
|
},
|
|
88
|
+
// get resolves with the payment for a reference, or undefined if there is none.
|
|
77
89
|
async checkPayment(state) {
|
|
78
90
|
const title = 'Check the last payment';
|
|
79
91
|
const found = await lookedUp(this.dash.payments.get(state.reference));
|
|
92
|
+
if (found.failed) {
|
|
93
|
+
this.setState({ error: found.outcome });
|
|
94
|
+
}
|
|
80
95
|
this.setState((current) => ({
|
|
81
96
|
...found.shown,
|
|
82
97
|
last: { title, expect: '', outcome: found.outcome },
|
|
@@ -86,6 +101,9 @@ export default definePage({
|
|
|
86
101
|
async lookUpUnknown() {
|
|
87
102
|
const title = 'Look up an order that never was';
|
|
88
103
|
const found = await lookedUp(this.dash.payments.get('order-that-never-was'));
|
|
104
|
+
if (found.failed) {
|
|
105
|
+
this.setState({ error: found.outcome });
|
|
106
|
+
}
|
|
89
107
|
this.setState((current) => ({
|
|
90
108
|
last: {
|
|
91
109
|
title,
|
|
@@ -102,11 +120,15 @@ async function lookedUp(lookup: Promise<PaymentResult | undefined>) {
|
|
|
102
120
|
try {
|
|
103
121
|
const result = await lookup;
|
|
104
122
|
if (result === undefined) {
|
|
105
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
outcome: 'undefined: there is no payment for this reference',
|
|
125
|
+
shown: {},
|
|
126
|
+
failed: false,
|
|
127
|
+
};
|
|
106
128
|
}
|
|
107
|
-
return { outcome: resultOutcome(result), shown: shownResult(result) };
|
|
129
|
+
return { outcome: resultOutcome(result), shown: shownResult(result), failed: false };
|
|
108
130
|
} catch (error) {
|
|
109
|
-
return { outcome: errorOutcome(error), shown: {} };
|
|
131
|
+
return { outcome: errorOutcome(error), shown: {}, failed: true };
|
|
110
132
|
}
|
|
111
133
|
}
|
|
112
134
|
|
|
@@ -114,6 +136,8 @@ function resultOutcome(result: PaymentResult): string {
|
|
|
114
136
|
return `${result.status} by ${result.method}, ${moneyText(result.amount)}`;
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
// isDashError(error) is true for an error from a Dash call, so it has a code. Give it codes, such
|
|
140
|
+
// as isDashError(error, 'invalid'), to check for those only.
|
|
117
141
|
function errorOutcome(error: unknown): string {
|
|
118
142
|
if (isDashError(error)) {
|
|
119
143
|
return `${error.code}: ${error.message}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">dash.payments</Text>
|
|
3
|
-
<Text>Takes a payment for an order through Dash's own payment sheet. The person picks a linked account, a card or mobile money, and the money goes to the Space that owns the dot. It needs the payments permission in dot.json. Each case below says what should happen. In Dash the money is real, so the orders cost a few cents.</Text>
|
|
3
|
+
<Text>Takes a payment for an order through Dash's own payment sheet. The person picks a linked account, a card or mobile money, and the money goes to the Space that owns the dot. It needs the payments permission in dot.json, which Dash doesn't ask the person for. Each case below says what should happen. In Dash the money is real, so the orders cost a few cents.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">The order</Text>
|
|
@@ -68,4 +68,6 @@
|
|
|
68
68
|
</List>
|
|
69
69
|
</View>
|
|
70
70
|
</If>
|
|
71
|
+
|
|
72
|
+
<Toast message="{{ error }}" tone="danger" duration="{{ 6000 }}" onHide="clearError" />
|
|
71
73
|
</View>
|