@monimesl/dot-cli 1.0.0-beta.1 → 1.0.0-beta.3
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 +13 -11
- package/dist/cli.js.map +1 -1
- package/dist/templates/showcase/.env +2 -2
- package/dist/templates/showcase/dot.ts +2 -1
- 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/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 +5 -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 +3 -1
- package/dist/templates/showcase/pages/components/layout/view.dml +5 -5
- package/dist/templates/showcase/pages/components/lists/view.dml +8 -8
- 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/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 +3 -3
- package/dist/templates/showcase/pages/components/tabs/state.ts +2 -2
- package/dist/templates/showcase/pages/components/tabs/view.dml +2 -2
- package/dist/templates/showcase/pages/components/text/view.dml +3 -3
- 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 +3 -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 +6 -1
- package/dist/templates/showcase/pages/dash/files/view.dml +1 -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 +18 -18
- package/dist/templates/showcase/pages/dash/payments/controller.ts +21 -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/secrets/controller.ts +3 -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 +3 -3
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// An article's body in Markdown, as a news app would get it from its server. It's written as
|
|
2
|
+
// joined lines so that the built code keeps it on one line.
|
|
3
3
|
const body = [
|
|
4
4
|
"## What's on sale",
|
|
5
5
|
'',
|
|
@@ -7,7 +7,7 @@ export default definePage({
|
|
|
7
7
|
openRelease: (state, event: DataEvent<{ id: string }>) => ({
|
|
8
8
|
selected: state.releases.find((release) => release.id === event.data.id) ?? null,
|
|
9
9
|
}),
|
|
10
|
-
// A
|
|
10
|
+
// A Sheet doesn't close itself. The page sets open back to false.
|
|
11
11
|
closeRelease: () => ({ selected: null }),
|
|
12
12
|
openRating: () => ({ rating: true }),
|
|
13
13
|
closeRating: () => ({ rating: false }),
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<View gap="small">
|
|
16
16
|
<Text variant="subtitle">A sheet with a form</Text>
|
|
17
|
-
<Text variant="caption" tone="muted">Any component can go in a sheet. This one has no close icon, and more room
|
|
17
|
+
<Text variant="caption" tone="muted">Any component can go in a sheet. This one has no close icon, and padding="large" gives it more room.</Text>
|
|
18
18
|
<View variant="card" direction="row" align="center" gap="small">
|
|
19
19
|
<View grow>
|
|
20
20
|
<Text>{{ saved ? 'You gave ' + saved + ' stars' : 'Not rated yet' }}</Text>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Styles</Text>
|
|
3
|
-
<Text>A View
|
|
3
|
+
<Text>A View can set its colour, outline, corners and size. Its colours come from the theme, so a dot looks right in light and dark.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">background</Text>
|
|
7
|
-
<Text variant="caption" tone="muted">
|
|
7
|
+
<Text variant="caption" tone="muted">On a primary background, give text tone="onPrimary" so it reads.</Text>
|
|
8
8
|
<View background="primary" radius="large" padding="large" gap="small">
|
|
9
9
|
<Text variant="caption" tone="onPrimary">Your balance</Text>
|
|
10
10
|
<Text variant="display" tone="onPrimary" numeric>{{ balance }}</Text>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
<View gap="small">
|
|
42
42
|
<Text variant="subtitle">Theme colours</Text>
|
|
43
|
-
<Text variant="caption" tone="muted">The theme in dot.ts can set primary, background, surface, text, muted, border and subtle, for light and dark
|
|
43
|
+
<Text variant="caption" tone="muted">The theme in dot.ts can set primary, background, surface, text, muted, border and subtle, for light and dark. Left out, they're Dash's. The theme can also name its own colours, such as gold and ink here. Use them as a background or a text tone.</Text>
|
|
44
44
|
<View direction="row" gap="small" align="center">
|
|
45
45
|
<View background="gold" radius="full" paddingTop="small" paddingBottom="small" paddingStart="medium" paddingEnd="medium">
|
|
46
46
|
<Text weight="medium" tone="ink">Gold member</Text>
|
|
@@ -40,7 +40,6 @@ const releases: Release[] = [
|
|
|
40
40
|
release('nack', 'Nack', 'The Therapist', 2022, 'song', 'Afropop'),
|
|
41
41
|
release('hannah', 'Hannah', 'Donii Major', 2021, 'song', 'R&B'),
|
|
42
42
|
release('shukubly', 'Shukubly', 'Drizilik', 2018, 'album', 'Kriofusion'),
|
|
43
|
-
release('en-yay-sah', 'En Yay Sah', 'Janka Nabay and the Bubu Gang', 2012, 'album', 'Bubu'),
|
|
44
43
|
release('red-album', 'Red Album', 'LAJ', 2013, 'album', 'Hip hop'),
|
|
45
44
|
release(
|
|
46
45
|
'ghetto-struggle',
|
|
@@ -50,6 +49,7 @@ const releases: Release[] = [
|
|
|
50
49
|
'song',
|
|
51
50
|
'Hip hop',
|
|
52
51
|
),
|
|
52
|
+
release('en-yay-sah', 'En Yay Sah', 'Janka Nabay and the Bubu Gang', 2012, 'album', 'Bubu'),
|
|
53
53
|
release('borbor-bele', 'Borbor Bele', 'Emmerson', 2004, 'album', 'Afropop'),
|
|
54
54
|
release('corruption', 'Corruption', 'Daddy Saj', 2003, 'album', 'Hip hop'),
|
|
55
55
|
release('cry-africa', 'Cry Africa', 'Vicky Fornah', 1999, 'song', 'Gospel'),
|
|
@@ -60,7 +60,7 @@ const releases: Release[] = [
|
|
|
60
60
|
|
|
61
61
|
export default {
|
|
62
62
|
releases,
|
|
63
|
-
// The releases the tab and the
|
|
63
|
+
// The releases left after the tab, the genre and the search filter them.
|
|
64
64
|
shown: releases,
|
|
65
65
|
tab: 'all',
|
|
66
66
|
albums: releases.filter((item) => item.kind === 'album').length,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Tabs and chips</Text>
|
|
3
|
-
<Text>Tabs change what the page shows. Chips and a search narrow it down. Try Songs with Palm-wine
|
|
3
|
+
<Text>Tabs change what the page shows. Chips and a search narrow it down. Try Songs with Palm-wine. Nothing matches, so an EmptyState shows.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">Tabs and Chip</Text>
|
|
7
|
-
<Text variant="caption" tone="muted">
|
|
7
|
+
<Text variant="caption" tone="muted">Tabs sends onChange with the chosen tab's value in event.detail.value. A Tab's badge shows a count. A Chip is selected only when the page says so. An Input with type="search" and clearable makes a search box.</Text>
|
|
8
8
|
<Tabs variant="underline" value="{{ tab }}" onChange="chooseTab">
|
|
9
9
|
<Tab value="all" label="All" />
|
|
10
10
|
<Tab value="album" label="Albums" badge="{{ albums }}" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Text and links</Text>
|
|
3
|
-
<Text>Text shows words. Its variant sets the size, and
|
|
3
|
+
<Text>Text shows words. Its variant sets the size and weight, and its tone sets the colour from the theme.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">variant</Text>
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
<View gap="small">
|
|
67
67
|
<Text variant="subtitle">leading and selectable</Text>
|
|
68
|
-
<Text variant="caption" tone="muted">leading moves the lines closer or further apart. selectable lets the person copy the text
|
|
68
|
+
<Text variant="caption" tone="muted">leading moves the lines closer or further apart. selectable lets the person select and copy the text. Hold your finger on the last one.</Text>
|
|
69
69
|
<View variant="card" gap="medium">
|
|
70
70
|
<Text leading="tight">Tight lines. Palm-wine music grew in the bars of Freetown, played on guitar and sung in Krio.</Text>
|
|
71
71
|
<Text leading="normal">Normal lines. Palm-wine music grew in the bars of Freetown, played on guitar and sung in Krio.</Text>
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
|
|
83
83
|
<View gap="small">
|
|
84
84
|
<Text variant="subtitle">Link</Text>
|
|
85
|
-
<Text variant="caption" tone="muted">With href, a
|
|
85
|
+
<Text variant="caption" tone="muted">With an https href, a Link opens the web page in the phone's browser. Without one, onTap runs a handler.</Text>
|
|
86
86
|
<View variant="card" gap="small">
|
|
87
87
|
<Text>Read about <Link href="https://en.wikipedia.org/wiki/Music_of_Sierra_Leone">the music of Sierra Leone</Link>.</Text>
|
|
88
88
|
<View direction="row" gap="medium">
|
|
@@ -9,6 +9,8 @@ export default definePage({
|
|
|
9
9
|
setNonce: (_state, event: ValueEvent) => ({ nonce: event.detail.value }),
|
|
10
10
|
},
|
|
11
11
|
actions: {
|
|
12
|
+
// A nonce longer than 128 characters rejects with 'invalid'. If no one is signed in to Dash, or
|
|
13
|
+
// Dash can't reach Monime, it rejects with 'failed'.
|
|
12
14
|
async signIn(state) {
|
|
13
15
|
try {
|
|
14
16
|
const { fullName, alias, accountIdToken } = await this.dash.account.signIn({
|
|
@@ -20,6 +22,7 @@ export default definePage({
|
|
|
20
22
|
this.setState({ note: errorMessage(error) });
|
|
21
23
|
}
|
|
22
24
|
},
|
|
25
|
+
// Rejects with 'unsupported' until Dash has OpenID Connect.
|
|
23
26
|
async signInWithOidc() {
|
|
24
27
|
try {
|
|
25
28
|
const { id } = await this.dash.account.signIn({ mode: 'oidc', scopes: ['profile'] });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
6
|
<Text variant="subtitle">signIn</Text>
|
|
7
|
-
<Text variant="caption" tone="muted">With mode "simple", Dash says who the person is: their full name, the phone number or email address they use Dash with
|
|
7
|
+
<Text variant="caption" tone="muted">With mode "simple", Dash says who the person is: their full name, and the phone number or email address they use Dash with. It also gives a token that names them in this dot. Monime makes it from the dot's id, the person and the nonce, so the same nonce gives the same token on any phone. The token names the person, but proves nothing to the dot's server.</Text>
|
|
8
8
|
<View variant="card" gap="medium">
|
|
9
9
|
<Input label="Nonce" value="{{ nonce }}" onChange="setNonce" />
|
|
10
10
|
<Button onClick="signIn">Sign in with Dash</Button>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
<View gap="small">
|
|
22
22
|
<Text variant="subtitle">signIn with OpenID Connect</Text>
|
|
23
|
-
<Text variant="caption" tone="muted">With mode "oidc", Dash will give an ID token the dot's server checks with Monime's keys, and only the details the person agrees to share. It isn't ready yet, so
|
|
23
|
+
<Text variant="caption" tone="muted">With mode "oidc", Dash will give an ID token the dot's server checks with Monime's keys, and only the details the person agrees to share. It isn't ready yet, so the call fails with unsupported.</Text>
|
|
24
24
|
<Button variant="secondary" onClick="signInWithOidc">Sign in with OpenID Connect</Button>
|
|
25
25
|
<If condition="{{ oidcNote }}">
|
|
26
26
|
<Notice tone="info">{{ oidcNote }}</Notice>
|
|
@@ -12,11 +12,14 @@ export default definePage({
|
|
|
12
12
|
await this.actions.refresh();
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
|
+
// Values are JSON: text, numbers, true or false, lists and objects. Each can be up to 1 MB,
|
|
16
|
+
// and the cache up to 64 MB. A full cache rejects with 'failed'.
|
|
15
17
|
actions: {
|
|
16
18
|
async refresh() {
|
|
17
19
|
const keys = await this.dash.storage.cache.keys();
|
|
18
20
|
const recent: string[] = [];
|
|
19
21
|
for (const key of keys.filter((each) => each.startsWith(RECENT))) {
|
|
22
|
+
// get gives null for a key that's missing or expired.
|
|
20
23
|
recent.push((await this.dash.storage.cache.get<string>(key)) ?? '');
|
|
21
24
|
}
|
|
22
25
|
const hasGenre = await this.dash.storage.cache.exists(GENRE);
|
|
@@ -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" />
|
|
@@ -14,6 +14,8 @@ export default definePage({
|
|
|
14
14
|
mutations: {
|
|
15
15
|
setDraft: (_state, event: ValueEvent) => ({ draft: event.detail.value }),
|
|
16
16
|
},
|
|
17
|
+
// Paths are inside the dot's folder, and '' is the folder itself. A call on a path with nothing
|
|
18
|
+
// there rejects with 'missing', while stat gives null.
|
|
17
19
|
actions: {
|
|
18
20
|
async refresh() {
|
|
19
21
|
const entries = await this.dash.storage.file.list('', { recursive: true });
|
|
@@ -33,6 +35,7 @@ export default definePage({
|
|
|
33
35
|
const documents = entries.filter(
|
|
34
36
|
(entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
|
|
35
37
|
);
|
|
38
|
+
// In bytes: what the dot's files take, and the most they may take.
|
|
36
39
|
const { used, quota } = await this.dash.storage.file.usage();
|
|
37
40
|
const kept = await this.dash.storage.file.stat(PICTURE);
|
|
38
41
|
this.setState({
|
|
@@ -54,11 +57,13 @@ export default definePage({
|
|
|
54
57
|
await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
|
|
55
58
|
await this.actions.refresh();
|
|
56
59
|
},
|
|
57
|
-
// Copies an archived setlist back, in place of the current one.
|
|
60
|
+
// Copies an archived setlist back, in place of the current one. Without overwrite, a path
|
|
61
|
+
// that's taken rejects with 'failed'.
|
|
58
62
|
async copyBack(_state, event: DataEvent<{ path: string }>) {
|
|
59
63
|
await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
|
|
60
64
|
await this.actions.refresh();
|
|
61
65
|
},
|
|
66
|
+
// A folder with files in it needs recursive.
|
|
62
67
|
async clearSetlist() {
|
|
63
68
|
await this.dash.storage.file.remove('setlist', { recursive: true });
|
|
64
69
|
await this.actions.refresh();
|
|
@@ -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>
|
|
@@ -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 }}">
|
|
@@ -26,8 +26,8 @@ export interface CaseGroup {
|
|
|
26
26
|
cases: PaymentCase[];
|
|
27
27
|
}
|
|
28
28
|
|
|
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.
|
|
29
|
+
// SLE 0.50 a ticket, in minor units as Monime counts money. In Dash the money is real, so the
|
|
30
|
+
// cases cost little: the two tickets the page starts with come to SLE 1.00.
|
|
31
31
|
const TICKET_PRICE = 50;
|
|
32
32
|
const SAMPLE_PHONE_NUMBER = '+23276123456';
|
|
33
33
|
const SAMPLE_ACCOUNT_ID = 'fac-k6VgVTfSmLruHpiQgMg4DUeySZx';
|
|
@@ -115,13 +115,13 @@ export const GROUPS: CaseGroup[] = [
|
|
|
115
115
|
id: 'orange',
|
|
116
116
|
title: 'Orange Money only',
|
|
117
117
|
expect: 'Mobile money with only Orange Money (m17).',
|
|
118
|
-
options: paying([{ type: 'momo', providers:
|
|
118
|
+
options: paying([{ type: 'momo', providers: ['m17'] }]),
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
id: 'africell',
|
|
122
122
|
title: 'Africell Money only',
|
|
123
123
|
expect: 'Mobile money with only Africell Money (m18).',
|
|
124
|
-
options: paying([{ type: 'momo', providers:
|
|
124
|
+
options: paying([{ type: 'momo', providers: ['m18'] }]),
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
id: 'one-number',
|
|
@@ -134,30 +134,30 @@ export const GROUPS: CaseGroup[] = [
|
|
|
134
134
|
),
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
|
-
id: '
|
|
138
|
-
title: '
|
|
139
|
-
expect: 'Linked accounts,
|
|
140
|
-
options: paying([{ type: 'link', providers:
|
|
137
|
+
id: 'any-provider',
|
|
138
|
+
title: 'An empty list of providers',
|
|
139
|
+
expect: 'Linked accounts at any provider, as an empty list means all of them.',
|
|
140
|
+
options: paying([{ type: 'link', providers: [] }]),
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
id: 'banks',
|
|
144
144
|
title: 'Bank accounts only',
|
|
145
145
|
expect: 'Linked accounts at slb001, slb004 and slb007 only, so no mobile money wallets.',
|
|
146
|
-
options: paying([{ type: 'link', providers:
|
|
146
|
+
options: paying([{ type: 'link', providers: ['slb001', 'slb004', 'slb007'] }]),
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
id: 'orange-or-account',
|
|
150
150
|
title: 'Orange Money from one number, or an account',
|
|
151
151
|
expect:
|
|
152
|
-
'Mobile money limited to Orange Money and the number above, then linked accounts
|
|
152
|
+
'Mobile money limited to Orange Money and the number above, then linked accounts at slb001 only.',
|
|
153
153
|
options: (input, reference) =>
|
|
154
154
|
paying([
|
|
155
155
|
{
|
|
156
156
|
type: 'momo',
|
|
157
|
-
providers:
|
|
157
|
+
providers: ['m17'],
|
|
158
158
|
phoneNumber: input.phoneNumber || SAMPLE_PHONE_NUMBER,
|
|
159
159
|
},
|
|
160
|
-
{ type: 'link', providers:
|
|
160
|
+
{ type: 'link', providers: ['slb001'] },
|
|
161
161
|
])(input, reference),
|
|
162
162
|
},
|
|
163
163
|
],
|
|
@@ -299,7 +299,7 @@ export const GROUPS: CaseGroup[] = [
|
|
|
299
299
|
id: 'invalid',
|
|
300
300
|
title: 'Refused before Dash',
|
|
301
301
|
subtitle:
|
|
302
|
-
'Every host checks these the same way
|
|
302
|
+
'Every host checks these the same way. Each rejects as invalid, with a message that starts as shown.',
|
|
303
303
|
cases: [
|
|
304
304
|
{
|
|
305
305
|
id: 'no-items',
|
|
@@ -399,16 +399,16 @@ export const GROUPS: CaseGroup[] = [
|
|
|
399
399
|
options: paying([{ type: 'card' }, { type: 'card' }]),
|
|
400
400
|
},
|
|
401
401
|
{
|
|
402
|
-
id: '
|
|
403
|
-
title: '
|
|
404
|
-
expect:
|
|
405
|
-
options: paying([{ type: 'link', providers:
|
|
402
|
+
id: 'providers-not-a-list',
|
|
403
|
+
title: 'Providers that are not a list',
|
|
404
|
+
expect: 'invalid: link\'s providers must be a list of provider IDs, such as ["m17"]',
|
|
405
|
+
options: paying([{ type: 'link', providers: 'm17' as unknown as string[] }]),
|
|
406
406
|
},
|
|
407
407
|
{
|
|
408
408
|
id: 'unknown-provider',
|
|
409
409
|
title: "A provider Monime doesn't know",
|
|
410
410
|
expect: 'invalid: "m99" isn\'t a provider Monime knows for momo',
|
|
411
|
-
options: paying([{ type: 'momo', providers:
|
|
411
|
+
options: paying([{ type: 'momo', providers: ['m99'] }]),
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
414
|
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,6 +52,9 @@ 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));
|
|
55
59
|
const cancelAfter = paymentCase.cancelAfterMilliseconds;
|
|
56
60
|
if (cancelAfter !== undefined) {
|
|
@@ -67,6 +71,7 @@ export default definePage({
|
|
|
67
71
|
this.setState(shownResult(result));
|
|
68
72
|
} catch (error) {
|
|
69
73
|
outcome = errorOutcome(error);
|
|
74
|
+
this.setState({ error: outcome });
|
|
70
75
|
}
|
|
71
76
|
// Another run may have started meanwhile, so this one names itself again.
|
|
72
77
|
this.setState((current) => ({
|
|
@@ -74,9 +79,13 @@ export default definePage({
|
|
|
74
79
|
runs: added(current.runs, paymentCase.title, outcome),
|
|
75
80
|
}));
|
|
76
81
|
},
|
|
82
|
+
// get resolves with the payment for a reference, or undefined if there is none.
|
|
77
83
|
async checkPayment(state) {
|
|
78
84
|
const title = 'Check the last payment';
|
|
79
85
|
const found = await lookedUp(this.dash.payments.get(state.reference));
|
|
86
|
+
if (found.failed) {
|
|
87
|
+
this.setState({ error: found.outcome });
|
|
88
|
+
}
|
|
80
89
|
this.setState((current) => ({
|
|
81
90
|
...found.shown,
|
|
82
91
|
last: { title, expect: '', outcome: found.outcome },
|
|
@@ -86,6 +95,9 @@ export default definePage({
|
|
|
86
95
|
async lookUpUnknown() {
|
|
87
96
|
const title = 'Look up an order that never was';
|
|
88
97
|
const found = await lookedUp(this.dash.payments.get('order-that-never-was'));
|
|
98
|
+
if (found.failed) {
|
|
99
|
+
this.setState({ error: found.outcome });
|
|
100
|
+
}
|
|
89
101
|
this.setState((current) => ({
|
|
90
102
|
last: {
|
|
91
103
|
title,
|
|
@@ -102,11 +114,15 @@ async function lookedUp(lookup: Promise<PaymentResult | undefined>) {
|
|
|
102
114
|
try {
|
|
103
115
|
const result = await lookup;
|
|
104
116
|
if (result === undefined) {
|
|
105
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
outcome: 'undefined: there is no payment for this reference',
|
|
119
|
+
shown: {},
|
|
120
|
+
failed: false,
|
|
121
|
+
};
|
|
106
122
|
}
|
|
107
|
-
return { outcome: resultOutcome(result), shown: shownResult(result) };
|
|
123
|
+
return { outcome: resultOutcome(result), shown: shownResult(result), failed: false };
|
|
108
124
|
} catch (error) {
|
|
109
|
-
return { outcome: errorOutcome(error), shown: {} };
|
|
125
|
+
return { outcome: errorOutcome(error), shown: {}, failed: true };
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
|
|
@@ -114,6 +130,8 @@ function resultOutcome(result: PaymentResult): string {
|
|
|
114
130
|
return `${result.status} by ${result.method}, ${moneyText(result.amount)}`;
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
// isDashError(error) is true for an error from a Dash call, so it has a code. Give it codes, such
|
|
134
|
+
// as isDashError(error, 'invalid'), to check for those only.
|
|
117
135
|
function errorOutcome(error: unknown): string {
|
|
118
136
|
if (isDashError(error)) {
|
|
119
137
|
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>
|
|
@@ -16,6 +16,7 @@ export default definePage({
|
|
|
16
16
|
},
|
|
17
17
|
actions: {
|
|
18
18
|
async refresh() {
|
|
19
|
+
// null if there's no secret by that name.
|
|
19
20
|
const token = await this.dash.storage.secret.get(SESSION);
|
|
20
21
|
const signedInAs = await this.dash.storage.cache.get<string>(NAME);
|
|
21
22
|
this.setState({
|
|
@@ -23,6 +24,8 @@ export default definePage({
|
|
|
23
24
|
signedInAs: signedInAs ?? '',
|
|
24
25
|
});
|
|
25
26
|
},
|
|
27
|
+
// A value over 4 KB rejects with 'invalid'. A 33rd secret rejects with 'failed', so remove
|
|
28
|
+
// one first.
|
|
26
29
|
async signIn(state) {
|
|
27
30
|
// A real dot gets its token from its own API, once the person signs in there.
|
|
28
31
|
const token = `demo-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
@@ -37,6 +37,7 @@ export default definePage({
|
|
|
37
37
|
this.setState({ note: errorMessage(error) });
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
+
// Only https addresses. Anything else rejects with 'invalid'.
|
|
40
41
|
async openDrizilik() {
|
|
41
42
|
try {
|
|
42
43
|
await this.dash.system.openUrl('https://drizilik.com/');
|
|
@@ -44,6 +45,7 @@ export default definePage({
|
|
|
44
45
|
this.setState({ note: errorMessage(error) });
|
|
45
46
|
}
|
|
46
47
|
},
|
|
48
|
+
// A dot's logic has no timers, so Dash keeps the time.
|
|
47
49
|
async countDown() {
|
|
48
50
|
for (let seconds = 10; seconds > 0; seconds--) {
|
|
49
51
|
this.setState({ showIn: seconds });
|
|
@@ -68,6 +70,8 @@ export default definePage({
|
|
|
68
70
|
this.setState({ note: errorMessage(error) });
|
|
69
71
|
}
|
|
70
72
|
},
|
|
73
|
+
// A number may have digits, spaces, dashes, +, * and #. Anything else rejects with 'invalid'.
|
|
74
|
+
// For a USSD code, Dash asks the person first, and rejects with 'cancelled' if they say no.
|
|
71
75
|
async dialNumber(state) {
|
|
72
76
|
try {
|
|
73
77
|
await this.dash.system.dial(state.number);
|
|
@@ -76,6 +80,7 @@ export default definePage({
|
|
|
76
80
|
this.setState({ note: errorMessage(error) });
|
|
77
81
|
}
|
|
78
82
|
},
|
|
83
|
+
// A phone without a torch rejects with 'unsupported'.
|
|
79
84
|
async toggleTorch(state) {
|
|
80
85
|
const torchOn = !state.torchOn;
|
|
81
86
|
try {
|