@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.
Files changed (76) hide show
  1. package/README.md +22 -9
  2. package/dist/cli.js +13 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/templates/showcase/.env +2 -2
  5. package/dist/templates/showcase/dot.ts +2 -1
  6. package/dist/templates/showcase/pages/basics/bars/controller.ts +2 -2
  7. package/dist/templates/showcase/pages/basics/bars/view.dml +2 -1
  8. package/dist/templates/showcase/pages/basics/bindings/state.ts +2 -2
  9. package/dist/templates/showcase/pages/basics/bindings/view.dml +2 -2
  10. package/dist/templates/showcase/pages/basics/colors/controller.ts +1 -1
  11. package/dist/templates/showcase/pages/basics/concert/view.dml +1 -1
  12. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
  13. package/dist/templates/showcase/pages/basics/control-flow/view.dml +1 -1
  14. package/dist/templates/showcase/pages/basics/events/controller.ts +1 -1
  15. package/dist/templates/showcase/pages/basics/events/view.dml +1 -1
  16. package/dist/templates/showcase/pages/basics/hooks/controller.ts +2 -1
  17. package/dist/templates/showcase/pages/basics/hooks/view.dml +2 -2
  18. package/dist/templates/showcase/pages/basics/navigation/controller.ts +1 -0
  19. package/dist/templates/showcase/pages/basics/navigation/view.dml +1 -1
  20. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +3 -3
  21. package/dist/templates/showcase/pages/basics/share/view.dml +2 -2
  22. package/dist/templates/showcase/pages/basics/state/controller.ts +9 -0
  23. package/dist/templates/showcase/pages/basics/state/state.ts +3 -0
  24. package/dist/templates/showcase/pages/components/carousel/view.dml +2 -2
  25. package/dist/templates/showcase/pages/components/feedback/controller.ts +2 -1
  26. package/dist/templates/showcase/pages/components/feedback/state.ts +2 -2
  27. package/dist/templates/showcase/pages/components/feedback/view.dml +2 -2
  28. package/dist/templates/showcase/pages/components/forms/controller.ts +2 -2
  29. package/dist/templates/showcase/pages/components/forms/state.ts +1 -1
  30. package/dist/templates/showcase/pages/components/forms/view.dml +5 -5
  31. package/dist/templates/showcase/pages/components/icons/controller.ts +1 -0
  32. package/dist/templates/showcase/pages/components/icons/state.ts +1 -1
  33. package/dist/templates/showcase/pages/components/images/controller.ts +1 -1
  34. package/dist/templates/showcase/pages/components/images/state.ts +1 -1
  35. package/dist/templates/showcase/pages/components/images/view.dml +4 -4
  36. package/dist/templates/showcase/pages/components/layout/controller.ts +3 -1
  37. package/dist/templates/showcase/pages/components/layout/view.dml +5 -5
  38. package/dist/templates/showcase/pages/components/lists/view.dml +8 -8
  39. package/dist/templates/showcase/pages/components/money/controller.ts +1 -1
  40. package/dist/templates/showcase/pages/components/money/view.dml +2 -2
  41. package/dist/templates/showcase/pages/components/reading/state.ts +2 -2
  42. package/dist/templates/showcase/pages/components/sheets/controller.ts +1 -1
  43. package/dist/templates/showcase/pages/components/sheets/view.dml +1 -1
  44. package/dist/templates/showcase/pages/components/styles/view.dml +3 -3
  45. package/dist/templates/showcase/pages/components/tabs/state.ts +2 -2
  46. package/dist/templates/showcase/pages/components/tabs/view.dml +2 -2
  47. package/dist/templates/showcase/pages/components/text/view.dml +3 -3
  48. package/dist/templates/showcase/pages/dash/account/controller.ts +3 -0
  49. package/dist/templates/showcase/pages/dash/account/view.dml +2 -2
  50. package/dist/templates/showcase/pages/dash/cache/controller.ts +3 -0
  51. package/dist/templates/showcase/pages/dash/calls/controller.ts +3 -2
  52. package/dist/templates/showcase/pages/dash/contacts/controller.ts +1 -0
  53. package/dist/templates/showcase/pages/dash/crypto/controller.ts +2 -0
  54. package/dist/templates/showcase/pages/dash/crypto/view.dml +1 -1
  55. package/dist/templates/showcase/pages/dash/files/controller.ts +6 -1
  56. package/dist/templates/showcase/pages/dash/files/view.dml +1 -1
  57. package/dist/templates/showcase/pages/dash/media/controller.ts +7 -1
  58. package/dist/templates/showcase/pages/dash/media/state.ts +2 -0
  59. package/dist/templates/showcase/pages/dash/network/controller.ts +5 -1
  60. package/dist/templates/showcase/pages/dash/network/view.dml +1 -1
  61. package/dist/templates/showcase/pages/dash/payments/cases.ts +18 -18
  62. package/dist/templates/showcase/pages/dash/payments/controller.ts +21 -3
  63. package/dist/templates/showcase/pages/dash/payments/state.ts +2 -0
  64. package/dist/templates/showcase/pages/dash/payments/view.dml +3 -1
  65. package/dist/templates/showcase/pages/dash/secrets/controller.ts +3 -0
  66. package/dist/templates/showcase/pages/dash/system/controller.ts +5 -0
  67. package/dist/templates/showcase/pages/dash/system/view.dml +4 -4
  68. package/dist/templates/showcase/pages/dash/ui/controller.ts +6 -1
  69. package/dist/templates/showcase/pages/index/controller.ts +1 -0
  70. package/dist/templates/showcase/pages/index/state.ts +3 -3
  71. package/dist/templates/starter/.env +2 -2
  72. package/dist/templates/starter/dot.ts +1 -0
  73. package/dist/templates/starter/pages/index/controller.ts +2 -0
  74. package/dist/templates/starter/pages/index/state.ts +2 -0
  75. package/dist/templates/starter/pages/index/view.dml +2 -1
  76. package/package.json +6 -6
@@ -1,5 +1,5 @@
1
- // As a news app would get an article's body from its server. The lines are joined here so the
2
- // production build stays on one line.
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 sheet doesn't close itself: the page sets `open` back to false.
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 inside.</Text>
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's colours, outlines, corners and sizes come from the theme, so a dot looks right in light and dark.</Text>
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">With primary, write text with tone="onPrimary" so it reads.</Text>
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, and a page can set its own with configs.colors. Left out, they're Dash's, or worked out from a new background. The theme can also name its own colours, such as gold and ink here, to use as a background or a text tone.</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. 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 chosen genre leave.
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: nothing matches.</Text>
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">The underline tabs, with a badge for each count. A chip is selected when the page says so. An Input with type="search" and clearable makes a search box.</Text>
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 the theme sets the font and colours.</Text>
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: hold your finger on the last one.</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 link opens a web page in the phone's browser. With onTap, it runs a handler.</Text>
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, and a token Monime makes from this dot's id, their Monimeer id and the nonce, with a key only it knows. The same nonce gives the same token, on any phone. It names the person, but proves nothing to the dot's server.</Text>
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 Dash says so.</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 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
- // Gives up after 1 second on a wait of 5.
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. Each gives hex, or base64 with { as: 'base64' }.</Text>
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
- // unlessCancelled gives undefined when the person closes the picker or scanner.
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
- // The catalog's artwork comes in any size its address names.
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, so trying a case with real money costs
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: { include: ['m17'] } }]),
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: { include: ['m18'] } }]),
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: 'not-a-bank',
138
- title: 'Account at any provider but one bank',
139
- expect: 'Linked accounts, leaving out slb004.',
140
- options: paying([{ type: 'link', providers: { exclude: ['slb004'] } }]),
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: { include: ['slb001', 'slb004', 'slb007'] } }]),
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 but slb004.',
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: { include: ['m17'] },
157
+ providers: ['m17'],
158
158
  phoneNumber: input.phoneNumber || SAMPLE_PHONE_NUMBER,
159
159
  },
160
- { type: 'link', providers: { exclude: ['slb004'] } },
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, so each rejects as invalid with the same message.',
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: 'both-filters',
403
- title: 'Include and exclude together',
404
- expect: "invalid: Give include or exclude for link's providers, not both",
405
- options: paying([{ type: 'link', providers: { include: ['m17'], exclude: ['m18'] } }]),
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: { include: ['m99'] } }]),
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 { outcome: 'undefined: there is no payment for this reference', shown: {} };
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}`;
@@ -30,4 +30,6 @@ export default {
30
30
  requestId: '',
31
31
  orderNumber: '',
32
32
  runs: [] as Run[],
33
+ // The last error, shown as a toast so it's seen without scrolling to the runs.
34
+ error: '',
33
35
  };
@@ -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 {