Products · Build a dot
Use Dash features
Reach the phone from a dot with this.dash, for alerts, photos, QR codes, contacts, reminders, the torch and sharing, and handle a call that fails.
Your dot can borrow the phone's camera, contacts, torch and more through this.dash. Each group's reference lists every option:
| Group | For |
|---|---|
dash.ui | Alerts, and date and time pickers |
dash.media | Photos, files and QR codes |
dash.contacts | A number from the person's contacts |
dash.account | Who the person is in Dash |
dash.system | The torch, the screen, copying, sharing, dialing |
dash.payments | Payments in Dash's own payment sheet |
dash.reminders | Notifications that bring the person back to one of your pages |
dash.crypto | Hashes and random values |
dash.utils | Base64 and hex |
Talking to your server and taking payments have their own pages: Network and storage and Take payments. Some calls need a permission, and each section below says which.
Use this.dash in pages
Call this.dash in a page's actions and hooks. When the page closes, Dash cancels any of its calls still running. Outside pages, as in dot.ts, use the global dash.
A call has no timeout unless you set one. Pass { timeout }, in milliseconds, as its last argument:
const request = this.dash.network.http.request({ url }, { timeout: 10_000 });
request.cancel(); // Or stop it yourself. It rejects with `cancelled`.Handle errors
A call that fails rejects with a DashError, and its code says why:
| Code | What happened | What to do |
|---|---|---|
unsupported | The phone can't do it | Hide the button |
denied | The call isn't allowed | Say what the feature needs |
cancelled | The person backed out, or the page closed | Usually nothing |
timeout | Your timeout ran out | Offer to try again |
invalid | The arguments are wrong | Fix the call |
missing | Nothing is at that path in the dot's files | Treat it as empty |
failed | The call couldn't finish. The phone may be offline | Show the message, and offer a retry |
A call is denied when the person said no to a permission, when the permission or host isn't in dot.json, or when Dash held the call back.
Check a code with isDashError. errorMessage turns any error into plain words you can show:
import { errorMessage, isDashError } from '@monimesl/dot-runtime';
try {
await this.dash.system.torch(true);
} catch (error) {
const note = isDashError(error, 'unsupported') ? 'This phone has no torch.' : errorMessage(error);
this.setState({ note });
}Pass several codes to match any of them: isDashError(error, 'timeout', 'failed'). Uncaught errors are covered in Pages and state.
When the person closes a picker
Closing a picker or scanner isn't a mistake, so don't show an error. unlessCancelled turns cancelled into undefined:
import { unlessCancelled } from '@monimesl/dot-runtime';
const date = await unlessCancelled(this.dash.ui.pickDate({ title: 'Collection day' }));
if (!date) {
return; // The person closed the calendar.
}When Dash holds a call back
To keep people in charge of their phone, Dash refuses some calls with denied:
- Too many dialogs. A dot can open at most 6 dialogs in any 30 seconds. Dialogs are alerts, date and time pickers, photo and file pickers, the scanner, contacts, share, dial and the payment sheet.
- Dash isn't on screen. While the person is in another app, a dot can't open dialogs, copy text, dial, share, open a link or ask for a payment. Try again once the page's
onShowhook runs.
Newer Dash features
Every call on this page works in any Dash that opens your dot. When a later Dash adds a call, check with this.dash.supports('contacts.pickPhone') first, so your dot still works in older Dash. monime dot dev warns you when you forget. See Versions.
Alerts
dash.ui.alert shows an alert and gives you the id of the button the person tapped. Leave out actions for a plain OK.
const { action } = await this.dash.ui.alert({
title: 'Cancel this order?',
actions: [
{ id: 'keep', label: 'Keep order', role: 'cancel' },
{ id: 'cancel', label: 'Cancel order', role: 'destructive' },
],
});The roles mark the button that backs out and the one that can't be undone. An empty actions list fails with invalid. Dash ends the message with a line naming your dot: "From Plasas Kitchen".
Dates and times
dash.ui.pickDate and dash.ui.pickTime open the phone's own calendar and clock. Dates look like 2026-10-24, and times like 19:30:
const date = await unlessCancelled(this.dash.ui.pickDate({ title: 'Collection day', min: '2026-10-14' }));min and max limit the dates the person can pick.
Photos, files and QR codes
In the simulator, you pick photos and files from your computer, and type a code instead of scanning it.
Photos
dash.media.pickPhoto takes a photo, which needs the camera permission, or picks one from the library, which needs photoLibrary:
const photo = await unlessCancelled(this.dash.media.pickPhoto({ source: 'camera' })); // or 'library'
if (photo) {
this.setState({ photo });
}You get a JPEG of at most 2048 pixels on its longest side, with its location removed.
Files the person chooses
dash.media.pickFile lets the person choose a file of up to 25 MB, like a PDF of their EDSA bill. types narrows the choice. It needs the files permission.
const file = await unlessCancelled(this.dash.media.pickFile({ types: ['application/pdf'] }));
if (file) {
await this.dash.storage.file.save(file, `bills/${file.name}`);
}Photos and files both arrive as a file object.
QR codes
dash.media.scanCode opens Dash's scanner and gives you the text it read. It needs the camera permission.
const code = await unlessCancelled(this.dash.media.scanCode());
if (code) {
this.setState({ meter: code.text });
}Contacts
Say the person is buying airtime for a cousin in Kenema. dash.contacts.pickPhone lets them choose the number from their contacts, and your dot sees only that one contact. It needs the contacts permission.
const contact = await unlessCancelled(this.dash.contacts.pickPhone());
if (contact) {
this.setState({ recipient: contact.name, phone: contact.phone });
}The number comes as it's saved, whether +232 76 123456 or 076 123456.
Sign the person in
Say your dot keeps a ferry pass for each regular. dash.account.signIn tells it who the person is in Dash, so they don't have to type their name and number. It needs the account permission, and Dash asks the person first.
// At the top of controller.ts:
// import { isDashError } from '@monimesl/dot-runtime';
// One of the page's actions
async signIn() {
try {
const person = await this.dash.account.signIn({ mode: 'simple' });
this.setState({
name: person.fullName,
phone: person.alias,
passId: person.accountIdToken,
});
} catch (error) {
if (!isDashError(error, 'denied')) {
throw error;
}
// They said no. Let them type their name and number.
this.setState({ typeDetails: true });
}
},| Field | What it is |
|---|---|
fullName | Their full name on their Dash account |
alias | The phone number they use Dash with, such as +232 76 123456, or their email address if they have no number |
accountIdToken | An ID for them in your dot. It's the same on any phone, and different in every other dot, so dots can't match up their customers |
To get an ID for this sign-in only, pass a fresh nonce, up to 128 characters. The same nonce always gives the same ID, so leave it out to know the person by one ID every time.
signIn fails with failed when no one is signed in to Dash or Dash can't reach Monime.
Reminders
A reminder is a notification Dash shows at a time you set, such as the morning school fees are due. When the person taps it, Dash opens your dot at the page you chose. Nothing happens until they tap.
await this.dash.reminders.set({
key: 'fees-term-3',
title: 'School fees are due today',
message: 'Tap to pay for Fatmata.',
page: '/pay?student=fatmata',
schedule: { start: '2026-11-27', time: '08:00' },
});keyis your own name for the reminder. Setting a reminder with the same key changes it, andcancel(key)removes it.pageis/and the page's ID, with any launch options as a query. Leave it out to open your first page.scheduletakes a start date and a time on the phone's clock, 09:00 if you leave it out. Addrepeatto repeat by day, week or month, andendto stop on a date or after so many times. SeeReminderSchedule.- A dot can have 12 reminders still to come at a time.
- People can delete reminders in Dash. Call
list()to see which are left before you show one as set.
It needs the reminders permission. Dash asks the first time you set a reminder. cancel and list only need the permission declared.
Phone features
dash.system reaches the rest of the phone:
| Call | What it does |
|---|---|
vibrate({ style: 'success' }) | A short buzz |
copyText('AC-2041') | Copies a payment reference or other text |
share({ text, url, file }) | Opens the share sheet |
dial('*144#') | Opens the dialer with a number or USSD code ready |
openUrl(url) | Opens an https page in the browser |
torch(true) | Turns the torch on, or off with false |
brightness('max'), keepAwake(true) | Turns the screen all the way up, and keeps it on |
info() | The platform, language, time zone and Dash version |
delay(3000) | Waits, in milliseconds. Dots have no setTimeout |
For a USSD code, Dash asks the person first, and dial rejects with cancelled if they say no. Codes that set up call forwarding, such as **21*…#, always fail with denied.
For a ticket the gate scans off the screen, turn the screen up in the page's onShow hook and back to 'normal' in onHide.
A delay ends when the page closes, and so does any loop that waits on it.
Hashes, random values and base64
You'll mostly want this.dash.crypto.randomUUID(), which makes a good key for sending an order once. dash.crypto also has sha256, sha512, hmac and randomBytes. Use hmac only with a key your server issued for this person's session, never your own API key.
dash.utils.base64 and dash.utils.hex turn text into base64 or hex and back, as in dash.utils.base64.encode('Kenema'). They're synchronous, and you can call them in a mutation too. decode throws on text that isn't base64 or hex. Catch it when the text comes from outside.
Search as the person types
A search field sends change on every keystroke:
<Input type="search" placeholder="Find a shop" onChange="find" />Two helpers spare your server: debounce waits for the person to pause, and latest cancels the last search when a new one starts:
import { debounce, latest, unlessCancelled } from '@monimesl/dot-runtime';
type Shop = { id: string; name: string };
const typingPause = debounce(400);
const search = latest();
export default definePage({
initialState: { results: [] as Shop[] },
actions: {
async find(_state, event: ValueEvent) {
if (!(await typingPause(this.dash))) {
return; // Still typing.
}
const url = `${env.API_URL}/shops?q=${encodeURIComponent(event.detail.value)}`;
const answer = await unlessCancelled(search(this.dash.network.http.request<Shop[]>({ url, as: 'json' })));
// answer is undefined when a newer search took over.
if (answer) {
this.setState({ results: answer.body });
}
},
},
});