i99dash docs
Recipes

Prayer times (offline-first)

An Islamic prayer-times + Qibla app that computes everything locally with the adhan package, then optionally enriches with a declared-egress fetch to the Aladhan API. ~20 minutes.

This recipe builds the kind of app that should work in a tunnel: five daily prayer times and a Qibla bearing, computed entirely on-device from the car's location — no network needed. Then we add an optional enhancement that fetches a whole month's calendar from a third-party API, gated behind a single declared network origin, with a clean fallback when the car is offline.

The pattern generalises: compute what you can locally; reach the network only for what you genuinely can't, and only for declared origins.

What you'll exercise

  • Local prayer-time + Qibla math with the adhan npm package (zero network).
  • The location.read permission + navigator.geolocation for the car's coordinates.
  • An optional declared-egress fetch() to https://api.aladhan.com, with offline fallback to the local computation.

1 — Scaffold and add adhan

pnpm dlx i99dash init prayer --template vanilla
cd prayer
pnpm install
pnpm add adhan

adhan is a pure-math library — it ships no network code. Bundle it into your app (e.g. with esbuild, --format=iife --target=es2019 for broad WebView support).

2 — Manifest

The local-only version needs just the location permission. We add the Aladhan origin to network now so the optional step in §5 works; leave it out entirely if you only ever compute locally — then the app reaches no third-party network at all.

{
  "id": "prayer",
  "name": { "en": "Prayer Times & Qibla", "ar": "مواقيت الصلاة والقبلة" },
  "icon": "./icon.svg",
  "url": "https://miniapps.i99dash.app/prayer/",
  "version": "0.1.0",
  "category": "lifestyle",
  "safeWhileDriving": true,
  "permissions": ["location.read"],
  "network": ["https://api.aladhan.com"]
}

3 — Read the car's location

getContext() gives you locale + the active car. For coordinates, use the standard navigator.geolocation — the host gates it per-origin against the location.read permission you declared, so it works inside the head unit the same way it does in a browser preview (see the Location guide). Fall back to Makkah (the Kaaba) when no fix is available, so the app always renders something.

import { MiniAppClient } from 'i99dash';

const KAABA = { lat: 21.4225, lng: 39.8262 };

const client = MiniAppClient.fromWindow();

async function getCoords(): Promise<{ lat: number; lng: number }> {
  if (typeof navigator === 'undefined' || !navigator.geolocation) return KAABA;
  try {
    const pos = await new Promise<GeolocationPosition>((resolve, reject) =>
      navigator.geolocation.getCurrentPosition(resolve, reject, {
        enableHighAccuracy: false,
        timeout: 10_000,
        maximumAge: 60_000,
      }),
    );
    return { lat: pos.coords.latitude, lng: pos.coords.longitude };
  } catch {
    // permission denied, timeout, or a broken WebView geolocation stack
    return KAABA;
  }
}

4 — Compute prayer times + Qibla locally (no network)

This is the heart of the app, and it never touches the network. adhan takes coordinates, a date, and a calculation method, and returns the five prayer Dates. adhan.Qibla() returns the bearing to the Kaaba in degrees from true north.

import * as adhan from 'adhan';

export interface DayTimes {
  fajr: Date;
  sunrise: Date;
  dhuhr: Date;
  asr: Date;
  maghrib: Date;
  isha: Date;
}

export function computeLocal(coords: { lat: number; lng: number }, date = new Date()) {
  const acoords = new adhan.Coordinates(coords.lat, coords.lng);

  // Pick a calculation method appropriate to the region. MuslimWorldLeague
  // is a sensible default; expose this as a setting for the user later.
  const params = adhan.CalculationMethod.MuslimWorldLeague();
  params.madhab = adhan.Madhab.Shafi;

  const t = new adhan.PrayerTimes(acoords, date, params);
  const times: DayTimes = {
    fajr: t.fajr,
    sunrise: t.sunrise,
    dhuhr: t.dhuhr,
    asr: t.asr,
    maghrib: t.maghrib,
    isha: t.isha,
  };

  const qiblaBearing = adhan.Qibla(acoords); // degrees from true north

  return { times, qiblaBearing };
}

At this point you have a fully working, offline app: read coords, computeLocal(), render the five times and a compass arrow. Ship it without ever calling §5. Recompute when the day rolls over or the car moves to a meaningfully different location.

5 — Optional: enrich with a monthly calendar (declared egress)

Local computation already gives you every day's times — but say you want the provider's pre-formatted monthly calendar (e.g. to match a printed timetable a user recognises, or to cross-check your method). The Aladhan API returns a full month in one call. Because it's a third-party origin, it must be in manifest.network (we added it in §2), and we fall back to the local computation when the car is offline.

const ALADHAN = 'https://api.aladhan.com';

/// Fetch the whole month's calendar from Aladhan. Returns null on any
/// failure (offline, CSP block, bad response) so the caller can fall
/// back to local compute — the app never depends on the network.
export async function fetchMonthlyCalendar(
  coords: { lat: number; lng: number },
  year: number,
  month: number, // 1-12
): Promise<unknown | null> {
  try {
    const url =
      `${ALADHAN}/v1/calendar/${year}/${month}` +
      `?latitude=${coords.lat}&longitude=${coords.lng}&method=3`; // 3 = Muslim World League
    const res = await fetch(url, { signal: AbortSignal.timeout(8_000) });
    if (!res.ok) return null;
    const body = (await res.json()) as { code?: number; data?: unknown };
    return body.code === 200 ? (body.data ?? null) : null;
  } catch {
    // fetch() rejects with TypeError on a network failure OR a CSP block
    // (origin missing from manifest.network). Either way: fall back.
    return null;
  }
}

Wire it offline-first — local compute is the source of truth; the remote calendar is a bonus that degrades silently:

async function loadMonth(coords: { lat: number; lng: number }) {
  const now = new Date();

  // 1 — always have something: local compute, instantly, offline.
  const today = computeLocal(coords, now);
  renderToday(today);

  // 2 — opportunistically enrich. If this fails we already rendered.
  const remote = await fetchMonthlyCalendar(coords, now.getFullYear(), now.getMonth() + 1);
  if (remote) renderMonthGrid(remote);
  else markMonthGridUnavailable(); // "Showing today only — offline"
}

Why this shape

  • Offline-first is the right default for a prayer app. A driver in a tunnel or a dead-zone still needs to know when Maghrib is. Local adhan math guarantees that; the network is strictly additive.
  • One declared origin, clearly scoped. network: ["https://api.aladhan.com"] is the entire egress surface. A reviewer (and you) can see exactly what the app reaches.
  • The fetch is unauthenticated and that's fine here — Aladhan is a public API needing no key. Declared egress attaches no i99dash credentials, so this is the ideal kind of upstream: public, keyless, read-only. (Needs a secret? Don't embed it — front the upstream with your own service and declare that. See Calling an external API.)

On this page