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:

GroupFor
dash.uiAlerts, and date and time pickers
dash.mediaPhotos, files and QR codes
dash.contactsA number from the person's contacts
dash.accountWho the person is in Dash
dash.systemThe torch, the screen, copying, sharing, dialing
dash.paymentsPayments in Dash's own payment sheet
dash.remindersNotifications that bring the person back to one of your pages
dash.cryptoHashes and random values
dash.utilsBase64 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:

TypeScript
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:

CodeWhat happenedWhat to do
unsupportedThe phone can't do itHide the button
deniedThe call isn't allowedSay what the feature needs
cancelledThe person backed out, or the page closedUsually nothing
timeoutYour timeout ran outOffer to try again
invalidThe arguments are wrongFix the call
missingNothing is at that path in the dot's filesTreat it as empty
failedThe call couldn't finish. The phone may be offlineShow 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:

TypeScript
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:

TypeScript
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 onShow hook 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.

TypeScript
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:

TypeScript
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:

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
// 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 });
  }
},
FieldWhat it is
fullNameTheir full name on their Dash account
aliasThe phone number they use Dash with, such as +232 76 123456, or their email address if they have no number
accountIdTokenAn 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.

TypeScript
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' },
});
  • key is your own name for the reminder. Setting a reminder with the same key changes it, and cancel(key) removes it.
  • page is / and the page's ID, with any launch options as a query. Leave it out to open your first page.
  • schedule takes a start date and a time on the phone's clock, 09:00 if you leave it out. Add repeat to repeat by day, week or month, and end to stop on a date or after so many times. See ReminderSchedule.
  • 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:

CallWhat 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:

pages/search/view.dml
<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:

pages/search/controller.ts
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 });
      }
    },
  },
});

Keep reading