JustToThePoint English Website Version
JustToThePoint en español

First‑Launch Seeding with AsyncStorage & SQLite

This is your life, and it’s ending one minute at a time, Chuck Palahniuk - Fight Club

Image 

AsyncStorage — the key/value store

AsyncStorage is React Native's persistent key-value database/store. It is equivalent to Python’s shelve module, browser localStorage, or a simple INI/config file.

PYTHON                              REACT NATIVE
──────────────────────────────────  ──────────────────────────────────────
import shelve                       import AsyncStorage from "@react-native-async-storage/async-storage"

db = shelve.open("prefs")           AsyncStorage is always open — no file handles, no context managers.
db["seeded"] = "true"               await AsyncStorage.setItem("seeded", "true"). Everything is async — every read/write returns a Promise.
value = db.get("seeded")            value = await AsyncStorage.getItem("seeded")
del db["seeded"]                    await AsyncStorage.removeItem("seeded")
db.close()                          (no close call needed)

When to use AsyncStorage vs SQLite

Use case Tool
Simple flags (“has the user seen the intro?”) AsyncStorage
Theme, font size, user preferences (small, simple, key/value) AsyncStorage
Structured data (prayers, pages, diary entries, notes) - we need tables, queries, and indexes SQLite
Large data (images, long text) expo-file-system (AsyncStorage is not designed for large data)

AsyncStorage stores strings only. To store a boolean or number, convert it: "true", "1", JSON.stringify(obj).

await AsyncStorage.setItem("flag", "true");
await AsyncStorage.setItem("count", String(42));
await AsyncStorage.setItem("prefs", JSON.stringify({ theme: "dark" }));

🌱 The seeding problem

When your app launches for the very first time, you want to insert a sample prayer into SQLite. The challenge is simple to state but tricky to implement: How do we know whether this is the first launch ever?

You need a signal that says: “The database has already been seeded — don’t seed again.” Let’s break down the three options and why only one is correct.

🅰️ Option A — Check SQLite row count

const count = await db.getAllAsync("SELECT COUNT(*) FROM myprayers");
if (count === 0) { seed(); }

Works, but what if the user deletes all their prayers? The seed would re-insert on next launch — probably not what they want.

🅱️ Option B — AsyncStorage flag ✅ (what we use)

const seeded = await AsyncStorage.getItem("prayers.seeded");
if (!seeded) {
  await seed();
  await AsyncStorage.setItem("prayers.seeded", "true");
}

AsyncStorage is a separate, persistent key/value store that survives app restarts and is independent of the database contents, meaning:

We are not asking: “Is the database empty?” We are indeed asking: “Has the app ever performed the seeding action?” In other words, that is an application event, not a database condition.

🅾️ Option C — Check for a flag file

This is the same idea as Option B, but implemented manually (more code, error handling). Write a file like seeded.txt into app storage. On launch, check if the file exists.

Step 1 — Install AsyncStorage

npx expo install @react-native-async-storage/async-storage

This package provides the same AsyncStorage that was in React Native core before it was extracted to a separate package. npx expo install ensures the version is compatible with your Expo SDK.

Step 2 — Create data/seed_prayers.json

📦 Why the seed data lives in a JSON file?

This file contains the initial prayer data that gets inserted into SQLite on the very first launch. Storing the seed data in a JSON file instead of hard‑coding it inside TypeScript gives you three big advantages:

  1. Separation of data from logic.
    Hardcoding seed data inside TypeScript mixes two things that should stay separate:
    Logic → the code that inserts data into SQLite. Data → the actual prayers, titles, page content, etc.
    By putting the data in a JSON file:
    The code stays clean and focused on how to seed. The JSON stays focused on what to seed.

  2. Easy to read, easy to edit. JSON is: simple, declarative, human‑readable, and non‑technical. Anyone can open the file and understand the structure.

  3. Adding more seed prayers becomes trivial.

  4. 🧠 The deeper reason: maintainability. Why? Because data changes more often than logic. Keeping them separate makes the app easier to evolve.

{
  "_comment": "Seed data inserted on first app launch. Edit to add more seed prayers.",
  "_format": "prayers[] each with title, description, date (use TODAY for today's date), pages[]",

  "prayers": [
    {
      "title": "Ignatian Morning Prayer",
      "description": "A five-step reflection to begin the day in God's presence.",
      "date": "TODAY",
      "image": null,
      "pages": [
        {
          "page_number": 1,
          "content": "<h2>Presence</h2><pgt;What I pay attention to has a hold on my becoming.</pgt;<pgt;I reflect on the presence of God in this moment, in my breath, in the light around me. I am not alone.</pgt;",
          "image": null
        },
        {
          "page_number": 2,
          "content": "<h2>Freedom</h2><pgt;Lord, free me from whatever holds me back from loving well today.</pgt;<pgt;I notice the fears, distractions or resentments in my heart, and I hand them to you. I ask for the grace to act freely.</pgt;",
          "image": null
        },
        {
          "page_number": 3,
          "content": "<h2>Gratitude</h2><pgt;I pause to notice the gifts of this new day.</pgt;<pgt;I thank you for rest, for another morning, for the people I will meet and the work I am given to do. Gratitude opens my heart.</pgt;",
          "image": null
        },
        {
          "page_number": 4,
          "content": "<h2>Petition</h2><pgt;Ask for what you need.</pgt;<pgt;I bring my needs and the needs of those I love into your presence, trusting that you hear me. I do not need to have the right words.</pgt;",
          "image": null
        },
        {
          "page_number": 5,
          "content": "<h2>Review</h2><pgt;At the close of the day, look back with gratitude.</pgt;<pgt;Where did I sense God's presence today? Where did I turn away? I rest in that awareness, without judgment, and trust in your mercy.</pgt;",
          "image": null
        }
      ]
    },
    {
      "title": "Evening Examen",
      "description": "The Ignatian examination of conscience — a review of the day.",
      "date": "TODAY",
      "image": null,
      "pages": [
        {
          "page_number": 1,
          "content": "<h2>Gratitude</h2><pgt;I give thanks for the gifts of this day — big and small.</pgt;<pgt;I recall a moment that brought joy, connection, or peace. I rest in it briefly.</pgt;",
          "image": null
        },
        {
          "page_number": 2,
          "content": "<h2>Review</h2><pgt;I walk slowly through the hours of the day.</pgt;<pgt;What happened? Who did I meet? Where did I feel most alive? Most drained? I do not judge — I simply observe.</pgt;",
          "image": null
        },
        {
          "page_number": 3,
          "content": "<h2>Sorrow</h2><pgt;I notice where I fell short of love today.</pgt;<pgt;A word spoken in haste. A moment of selfishness. I acknowledge it honestly and ask for forgiveness, knowing it is already given.</pgt;",
          "image": null
        },
        {
          "page_number": 4,
          "content": "<h2>Renewal</h2><pgt;I look ahead to tomorrow with hope.</pgt;<pgt;Is there something I want to do differently? A relationship I want to tend? I offer tomorrow to God before it begins.</pgt;",
          "image": null
        }
      ]
    }
  ]
}

Note the "date": "TODAY" value. We cannot put a real date in a static JSON file because the app is installed on different days by different people. The seed function will replace the string "TODAY" with the actual ISO date at runtime.

Step 3 — Create src/db/seedDatabase.ts

Now we write the function that orchestrates the seeding.

// src/db/seedDatabase.ts
//
// FIRST-LAUNCH DATABASE SEED
// ───────────────────────────
// Inserts sample prayers into the SQLite database the first time the app runs.
//
// WHEN IT RUNS
//   - First app launch after installation: seed data is inserted.
//   - All subsequent launches: skipped (AsyncStorage flag is set).
//   - After "Reset app data" (Settings screen): runs again.
//
//      c. Set "prayers.seeded" = "true" in AsyncStorage.
//
// SEED DATA FORMAT (seed_prayers.json)
//   {
//     "prayers": [
//       {
//         "title":       string,
//         "description": string,
//         "date":        "TODAY" | "YYYY-MM-DD",
//         "image":       null,
//         "pages": [
//           { "page_number": number, "content": string, "image": null }
//         ]
//       }
//     ]
//   }
//
//   The special value "TODAY" in the date field is replaced with
//   todayISO() at seed time so the prayer appears in today's list.

import AsyncStorage from "@react-native-async-storage/async-storage";
import type { SQLiteDatabase } from "expo-sqlite";
import { todayISO } from "../utils/dateUtils";
import { addPrayer, addContentPrayer } from "./prayerQueries";

// ── AsyncStorage key ──────────────────────────────────────────────────────────
// Centralise the key name so a typo cannot cause the seed to run twice.
const SEED_KEY = "prayers.seeded";

// ── Seed data types ───────────────────────────────────────────────────────────
// These match the shape of seed_prayers.json.
// They are LOCAL to this file — not exported, not in types.ts — because
// they only exist to parse the JSON file.

interface SeedPage {
  page_number: number;
  content:     string;
  image:       string | null;
}

interface SeedPrayer {
  title:       string;
  description: string;
  date:        string;   // "TODAY" or "YYYY-MM-DD"
  image:       string | null;
  pages:       SeedPage[];
}

// ── Validation helpers ────────────────────────────────────────────────────────

/**
 * Safely convert any string to a trimmed string.
 * Returns the fallback (default: empty string) if the value is null, undefined, or not a string.
 *
 */
function safeString(value: unknown, fallback = ""): string {
  if (typeof value === "string") return value.trim();
  return fallback;
}

/**
 * validateSeedPrayer() — the “gatekeeper”.
 * Validate one prayer from the seed JSON.
 * Throws a descriptive error if required fields are missing.
 */
function validateSeedPrayer(raw: unknown, index: number): SeedPrayer {
  /* (1) Ensure the prayer is an object */
  if (!raw || typeof raw !== "object") {
    throw new Error(`Seed prayer[${index}] is not an object.`);
  }

  const obj = raw as Record<string, unknown>;
  /* (2) Extract and validate the title. Ensures the title is non-empty
  If missing → throw a descriptive error */
  const title = safeString(obj.title);
  if (!title) {
    throw new Error(
      `Seed prayer[${index}] has no title. Got: ${JSON.stringify(obj.title)}`
    );
  }

  // (3) Validate pages (it has a pages array)
  if (!Array.isArray(obj.pages)) {
    throw new Error(
      `Seed prayer[${index}] "${title}" has no pages array.`
    );
  }

  // (4) Validate each page. Each page is an object,
  // page_number is a number (otherwise auto‑assign pi + 1),
  // content is a safe string, and image is either a string or null
  const pages: SeedPage[] = (obj.pages as unknown[]).map((p, pi) => {
    if (!p || typeof p !== "object") {
      throw new Error(`Seed prayer[${index}] page[${pi}] is not an object.`);
    }
    const page = p as Record<string, unknown>;
    return {
      page_number: typeof page.page_number === "number" ? page.page_number : pi + 1,
      content: safeString(page.content),
      image: typeof page.image === "string" ? page.image : null,
    };
  });

  // (5) The function returns a fully validated, fully typed:
  /* {
    title: string,
    description: string,
    date: string,
    image: string | null,
    pages: SeedPage[]
   } */
  return {
    title,
    description: safeString(obj.description),
    date: safeString(obj.date, "TODAY"),
    image: typeof obj.image === "string" ? obj.image : null,
    pages,
  };
}


// ══════════════════════════════════════════════════════════════════════════════
// PUBLIC API
// ══════════════════════════════════════════════════════════════════════════════

/**
 * Seed the database with sample prayers on first launch.
 *
 * Safe to call on every app start — it checks the AsyncStorage flag first
 * and exits immediately if seeding has already been done.
 *
 * @param db  Open SQLiteDatabase from useSQLiteContext()
 * @returns   Object describing what happened:
 *              { seeded: boolean, prayerCount: number, message: string }
 *
 * @example
 * // In app/_layout.tsx initDatabase or in a useEffect:
 * const result = await seedOnFirstLaunch(db);
 * console.log(result.message);
 * // "Seeded 2 prayers (10 pages total) on first launch."
 * // OR
 * // "Already seeded — skipping."
 */
export async function seedOnFirstLaunch(db: SQLiteDatabase): Promise<{
  seeded:       boolean;
  prayerCount:  number;
  message:      string;
}> {
  // ── How the seeding logic works ─────────────────────────────────────────────────
  // 1. Check the flag – AsyncStorage.getItem(SEED_KEY) returns null if the
  // key has never been written.
  const flag = await AsyncStorage.getItem(SEED_KEY);

  if (flag !== null) {
    // If it finds a value, the function prints a
    // message and returns immediately, leaving the database untouched.
    console.log("[Seed] Already seeded — skipping.");
    return { seeded: false, prayerCount: 0, message: "Already seeded — skipping." };
  }

  // ── Load seed data ──────────────────────────────────────────────────────────
  console.log("[Seed] First launch detected. Seeding database…");

  // 2. Load the JSON file.
  // This gives you the parsed JSON object.
  const raw: unknown = require("../../data/seed_prayers.json");

  // Log what we got so we can see it in Metro if something is wrong
  console.log("[Seed] require() returned type:", typeof raw);

  // 3. Validate top-level structure. Defensive safety check
  if (!raw || typeof raw !== "object") {
    const msg = `[Seed] seed_prayers.json did not return an object. Got: ${typeof raw}`;
    console.error(msg);
    return { seeded: false, prayerCount: 0, message: msg };
  }

  const seedFile = raw as Record<string, unknown>;

  // 4. Metro/Hermes sometimes wraps JSON modules with a `default` key
  // { default: { prayers: [...] } }
  // So the code checks both shapes:
  let prayers: unknown;
  if (Array.isArray(seedFile.prayers)) {
    prayers = seedFile.prayers;
  } else if (
    seedFile.default &&
    typeof seedFile.default === "object" &&
    Array.isArray((seedFile.default as Record<string, unknown>).prayers)
  ) {
    // Unwrap the default export if present
    prayers = (seedFile.default as Record<string, unknown>).prayers;
    console.log("[Seed] Unwrapped .default from JSON module.");
  } else {
    const msg = `[Seed] seed_prayers.json has no .prayers array. Keys: ${Object.keys(seedFile).join(", ")}`;
    console.error(msg);
    return { seeded: false, prayerCount: 0, message: msg };
  }

  const prayerArray = prayers as unknown[];
  if (prayerArray.length === 0) {
    await AsyncStorage.setItem(SEED_KEY, "true");
    return { seeded: true, prayerCount: 0, message: "Seed file has no prayers." };
  }

  // 5. Insert prayers and pages.
  let totalPages = 0;
  let inserted = 0;

  for (let i = 0; i < prayerArray.length; i++) {
    try {
      // We call the gatekeeper, validateSeedPrayer(). Validate one prayer from the seed JSON.
      const seedPrayer = validateSeedPrayer(prayerArray[i], i);
      // Replace the date placeholder – Any prayer with "date": "TODAY" gets a real ISO date
      // via the todayISO() helper. This means the seed prayer always appears in today’s list,
      // no matter when the app is installed.
      const date = seedPrayer.date === "TODAY" ? todayISO() : seedPrayer.date;
      // addPrayer insert the prayer row and returns the new id
      const prayerId = await addPrayer(
        db,
        seedPrayer.title,
        seedPrayer.description,
        date,
        seedPrayer.image
      );
      // addContentPrayer insert (writes) all pages for this prayer in a single transaction
      // We keep a count of total pages for the success message.
      if (seedPrayer.pages.length > 0) {
        await addContentPrayer(db, prayerId, seedPrayer.pages);
        totalPages += seedPrayer.pages.length;
      }
      // Log success or failure
      inserted++;
      console.log(
        `[Seed] Inserted "${seedPrayer.title}" ` +
        `(id=${prayerId}, ${seedPrayer.pages.length} pages)`
      );
    } catch (e) {
      console.error(`[Seed] Failed to insert prayer[${i}]: ${e}`);
      // Continue with remaining prayers rather than aborting
    }
  }

  // ── 6. Set the flag ───────────────────────────────────────────────────────
  // The flag is set only after all inserts succeed.
  // If the app crashes halfway, the flag remains unset and seeding will run
  // again on the next launch.
  // Because addPrayer uses INSERT (not INSERT OR IGNORE), you could get
  // duplicates in this rare case. A solution is discussed in later posts.
  await AsyncStorage.setItem(SEED_KEY, "true");

  // 7. Return summary
  const message = `Seeded ${inserted} of ${prayerArray.length} prayer(s) (${totalPages} pages).`;
  console.log(`[Seed] ${message}`);
  return { seeded: true, prayerCount: inserted, message };
}

/**
 * Reset the seed flag.
 * After calling this, seedOnFirstLaunch() will run again on the next call.
 *
 * Used by:
 *   - The test screen to test the seed without reinstalling
 *   - The Settings screen "Reset app data" button
 *
 * WARNING: resetSeedFlag() wipes the prayers.seeded key from AsyncStorage
 * but leaves the SQLite tables untouched. The next time seedOnFirstLaunch()
 * is called (e.g. on the next app start or manually) it will re‑run the
 * seeding logic. If you want a truly fresh start, pair this with a
 * DELETE FROM myprayers (and DELETE FROM prayers_pages) so the
 * database is clean.
 */
export async function resetSeedFlag(): Promise<void> {
  await AsyncStorage.removeItem(SEED_KEY);
  console.log("[Seed] Seed flag cleared. Will re-seed on next launch.");
}


/**
 * Check whether the database has been seeded.
 * It simply reads the AsyncStorage flag and returns true or false.
 *
 * @example
 * const already = await isSeedComplete();
 * if (!already) { console.log("Not yet seeded"); }
 */
export async function isSeedComplete(): Promise<boolean> {
  const flag = await AsyncStorage.getItem(SEED_KEY);
  return flag !== null;
}

Step 4 — Wire the seed into app/_layout.tsx

The seed must happen after the schema migrations (so the tables exist) and before any screen tries to read prayers.

The best place is inside initDatabase in src/db/database.ts because initDatabase is already called by SQLiteProvider.onInit and runs every time the database is opened.

Open src/db/database.ts and it as follows:

// src/db/database.ts — updated initDatabase function only
// (everything else stays the same)

import { seedOnFirstLaunch } from "./seedDatabase";  // ← ADD THIS IMPORT

export async function initDatabase(
  db: SQLite.SQLiteDatabase
): Promise<void> {
  // PRAGMAs first — must be outside any transaction
  await db.execAsync("PRAGMA journal_mode = WAL");
  await db.execAsync("PRAGMA foreign_keys = ON");

  // Run schema migrations
  await runMigrations(db);

  // Seed on first launch — safe to call every time (checks flag internally)
  await seedOnFirstLaunch(db);

  console.log("[DB] Database initialised successfully.");
}

Now the app flow is:

  1. SQLiteProvider opens the database.
  2. onInit calls initDatabase → runs migrations → calls seedOnFirstLaunch.
  3. seedOnFirstLaunch checks the flag; if missing, it reads the JSON, inserts prayers, and sets the flag. If the flag is already set, it logs “Already seeded — skipping.” and returns.
  4. After initDatabase resolves, the UI renders and sees the seed data.

This is the last piece of the puzzle for a reliable first‑launch experience.

Step 5 — Add .json to Metro’s asset extensions

Metro, the bundler used by Expo, must know that .json files should be treated as assets so that readAsset can find them. This was already done for .txt files; we now confirm that "json" is also listed.

// metro.config.js — confirm json is included
config.resolver.assetExts = [
  ...config.resolver.assetExts,
  "txt",
  "json",   // ← must be present for seed_prayers.json
];

Step 6 — Update app/(tabs)/prayers.tsx with the seed test screen

Replace the entire file:

// app/(tabs)/prayers.tsx
//
// PRAYERS TAB — Test screen
//
// Tests the seed system:
//   - Shows whether the database was seeded
//   - Shows the seeded prayers
//   - Provides "Reset seed flag" button to test re-seeding without reinstalling
//   - "Reset + re-seed" deletes all prayers and re-runs the seed

import React, { useEffect, useState, useCallback } from "react";
import {
  View, Text, ScrollView, StyleSheet,
  TouchableOpacity, ActivityIndicator, Alert,
} from "react-native";
import { useSQLiteContext } from "expo-sqlite";
import {
  seedOnFirstLaunch, resetSeedFlag, isSeedComplete,
} from "../../src/db/seedDatabase";
import {
  listPrayers, countPrayers,
} from "../../src/db/prayerQueries";
import type { PrayerSummary } from "../../src/db/types";

export default function PrayersScreen() {
  // Define the state
  const db = useSQLiteContext();
  const [loading,      setLoading]      = useState(true);
  // ✅ 1. Creates a state variable. seedComplete starts as false.
  // Creates a setter function. setSeedComplete(newValue) tells React:
  // “Please update this state and re-render the component"
  const [seedComplete, setSeedComplete] = useState(false);
  const [prayers,      setPrayers]      = useState<PrayerSummary[]>([]);
  const [total,        setTotal]        = useState(0);
  const [lastMsg,      setLastMsg]      = useState("");

  // ── Load current state ───────────────────────────────────────────────────────
  const loadState = useCallback(async () => {
    /* I’m about to fetch data — show the loading spinner. */
    setLoading(true);
    /* We are running three async operations at the same time:
    (1) isSeedComplete() checks AsyncStorage to see if the seeding has been completed
    (2) listPrayers(db, 20, 0) loads the first 20 prayers from SQLite
    (3) countPrayers(db) counts how many prayers exist in total
    React run them IN PARALLEL, waits until all three finish, then returns an array:
    [complete (boolean), list (array of prayers), count (number of prayers in DB)] */
    const [complete, list, count] = await Promise.all([
      isSeedComplete(),
      listPrayers(db, 20, 0),
      countPrayers(db),
    ]);
    /* Once the data arrives: (Updating React state) seed status, prayer list, and total count are updated
    React now has the latest: whether seeding is done, the list of prayers, and
    the total number of prayers */
    // ✅ 2. setSeedComplete(complete); Suppose complete is true. React does not update the variable immediately.
    // Instead, React schedules a re-render. Idea: Okay, I’ll update seedComplete to true on the next render.
    setSeedComplete(complete);
    setPrayers(list);
    setTotal(count);
    /* Data is ready — hide the spinner. UI re-renders with the new data */
    setLoading(false);
  }, [db]);

  /* When the screen first mounts → call loadState() (fetch the data)
  If the database instance ever changes → call loadState() again */
  useEffect(() => { loadState(); }, [loadState]);

  // ── Reset seed flag only  ──────────────────────────────
  /* Clears the AsyncStorage flag (prayers.seeded = false). It does NOT delete any prayers.
  Shows a message to the user. Reloads the UI state.
  Sometimes you may want to test the seeding logic again without wiping the database. */
  async function handleResetFlag() {
    await resetSeedFlag();
    setLastMsg("Seed flag cleared. Tap 'Re-seed now' to run the seed again.");
    await loadState();
  }

  // ── Full Reset + re-seed (delete everything and start over) ────────────────────────
  /* This is the nuclear option — wipe everything and start fresh. */
  async function handleFullReset() {
    /* 1. Show a confirmation dialog because this is destructive. */
    Alert.alert(
      "Delete all prayers?",
      "This will delete ALL prayers and re-seed the database. This cannot be undone.",
      [
        { text: "Cancel", style: "cancel" },
        {
          text: "Reset and re-seed",
          style: "destructive",
          /* 2. If the user confirms: (a) setLoading(true): It shows spinner.
          (b) DELETE FROM myprayers: It removes all prayers; pages are cascade-deleted automatically.
          (c) resetSeedFlag(): It clears the AsyncStorage flag.
          (d) seedOnFirstLaunch(db): Immediately re-seeds the database.
          (e) setLastMsg(result.message): Shows a message like “Seeded 2 prayers”.
          (f) loadState(): Refreshes the UI. */
          onPress: async () => {
            setLoading(true);
            await db.runAsync("DELETE FROM myprayers");
            await resetSeedFlag();
            const result = await seedOnFirstLaunch(db);
            setLastMsg(result.message);
            await loadState();
          },
        },
      ]
    );
  }

  // ── Re-seed now (without deleting existing prayers) ───────────────────────────
  /* Runs seedOnFirstLaunch(db). However, seedOnFirstLaunch checks the flag.
  If the flag is set, it does nothing. If the flag is cleared, it seeds. */
  async function handleReseed() {
    setLoading(true);
    const result = await seedOnFirstLaunch(db);
    setLastMsg(result.message);
    await loadState();
  }

  /* ⏳ 4. Loading screen
  If loading === true, the component returns early and shows a spinner.
  React does not render the rest of the screen until loading finishes. */
  if (loading) {
    return (
      <View style={s.center}>
        <ActivityIndicator size="large" color="#c9a84c" />
        <Text style={s.muted}>Loading…</Text>
      </View>
    );
  }

  /* Main UI. Everything below runs only when loading === false. */
  return (
    <ScrollView style={s.scroll} contentContainerStyle={s.content}>

      {/* Seed status card */}
      /* It shows: Whether the seed flag is set. How many prayers exist.
      The last message from any action (reset, reseed, etc.) */
      <View style={[s.statusCard, seedComplete ? s.statusOk : s.statusWarn]}>
        <Text style={seedComplete ? s.textOk : s.textWarn}>
          // ✅ 3. The UI updates automatically
          {seedComplete
            ? "✅  Database seeded"
            : "⚠️  Seed flag not set"}
        </Text>
        <Text style={s.statusSub}>{total} prayer(s) in database</Text>
        {lastMsg ? <Text style={s.lastMsg}>{lastMsg}</Text> : null}
      </View>

      {/* Prayer list */}
      <Text style={s.sectionTitle}>Prayers in database</Text>
      /* If no prayers → show an empty state card */
      {prayers.length === 0 ? (
        <View style={s.emptyCard}>
          <Text style={s.muted}>No prayers found.</Text>
          <Text style={s.muted}>Tap "Re-seed now" below.</Text>
        </View>
      ) : (
        /* Otherwise → render each prayer in a row */
        prayers.map((p) => (
          <View key={p.id_myprayer} style={s.prayerRow}>
            <View style={s.prayerInfo}>
              <Text style={s.prayerTitle}>{p.title}</Text>
              /* Why numberOfLines={1}? It prevents long descriptions from breaking the layout. */
              <Text style={s.prayerDesc} numberOfLines={1}>
                {p.description}
              </Text>
            </View>
            <Text style={s.prayerDate}>{p.date || "—"}</Text>
          </View>
        ))
      )}

      {/* Action buttons */}
      <Text style={s.sectionTitle}>Seed controls</Text>

      /* Re-seed now: runs seedOnFirstLaunch(). It does nothing if flag is set. Seeds if flag is cleared */
      <TouchableOpacity style={s.btn} onPress={handleReseed}>
        <Text style={s.btnText}>🌱  Re-seed now</Text>
        <Text style={s.btnSub}>Runs seedOnFirstLaunch() — skips if flag is set</Text>
      </TouchableOpacity>

      /* Reset seed flag only. Clears flag. It does NOT delete prayers */
      <TouchableOpacity style={[s.btn, s.btnSecondary]} onPress={handleResetFlag}>
        <Text style={s.btnText}>🏳  Reset seed flag only</Text>
        <Text style={s.btnSub}>Clears AsyncStorage flag, keeps existing prayers</Text>
      </TouchableOpacity>

      /* Full reset + re-seed. Deletes everything. Clears flag. Re-seeds */
      <TouchableOpacity style={[s.btn, s.btnDanger]} onPress={handleFullReset}>
        <Text style={s.btnText}>🗑  Full reset + re-seed</Text>
        <Text style={s.btnSub}>Deletes ALL prayers, clears flag, re-seeds</Text>
      </TouchableOpacity>

      {/* Explanation */}
      <View style={s.hint}>
        <Text style={s.hintText}>
          Post complete.{"\n\n"}
          How to test without reinstalling:{"\n"}
          1. Tap "Full reset + re-seed"{"\n"}
          2. Prayers disappear and reappear (2 seed prayers){"\n"}
          3. Restart the app — prayers are NOT duplicated{"\n"}
          4. Tap "Reset seed flag only", then restart{"\n"}
          5. App re-seeds on startup{"\n\n"}
        </Text>
      </View>

    </ScrollView>
  );
}

const s = StyleSheet.create({
  scroll:       { flex: 1, backgroundColor: "#1a1f2e" },
  content:      { padding: 20, paddingBottom: 48 },
  center:       { flex: 1, backgroundColor: "#1a1f2e",
                  alignItems: "center", justifyContent: "center", gap: 12 },
  muted:        { color: "#6b7280", fontSize: 13, textAlign: "center" },
  statusCard:   { borderRadius: 10, padding: 16, marginBottom: 20,
                  borderWidth: 1, alignItems: "center" },
  statusOk:     { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
  statusWarn:   { backgroundColor: "#2e2a1a", borderColor: "#4a3d2d" },
  statusSub:    { fontSize: 12, color: "#6b7280", marginTop: 4 },
  lastMsg:      { fontSize: 11, color: "#9ca3af", marginTop: 6,
                  fontFamily: "monospace", textAlign: "center" },
  textOk:       { color: "#86efac", fontSize: 15, fontWeight: "bold" },
  textWarn:     { color: "#fbbf24", fontSize: 15, fontWeight: "bold" },
  sectionTitle: { fontSize: 12, color: "#6b7280", fontFamily: "monospace",
                  marginBottom: 10, marginTop: 4 },
  emptyCard:    { backgroundColor: "#222840", borderRadius: 8, padding: 20,
                  alignItems: "center", gap: 6, marginBottom: 16 },
  prayerRow:    { backgroundColor: "#222840", borderRadius: 8, padding: 14,
                  marginBottom: 8, borderWidth: 1, borderColor: "#2d3550",
                  flexDirection: "row", alignItems: "center" },
  prayerInfo:   { flex: 1, marginRight: 12 },
  prayerTitle:  { fontSize: 14, fontWeight: "600", color: "#dde4f0",
                  marginBottom: 2 },
  prayerDesc:   { fontSize: 12, color: "#6b7280" },
  prayerDate:   { fontSize: 11, color: "#5b8fd4",
                  fontFamily: "monospace" },
  btn:          { backgroundColor: "#222840", borderRadius: 8, padding: 14,
                  marginBottom: 10, borderWidth: 1, borderColor: "#2d3550" },
  btnSecondary: { borderColor: "#3d4a2d" },
  btnDanger:    { borderColor: "#4a2d2d" },
  btnText:      { color: "#dde4f0", fontSize: 14, fontWeight: "600",
                  marginBottom: 2 },
  btnSub:       { color: "#6b7280", fontSize: 11 },
  hint:         { backgroundColor: "#1a1f2e", borderRadius: 8, padding: 14,
                  borderWidth: 1, borderColor: "#2d3550", marginTop: 8 },
  hintText:     { fontSize: 11, color: "#4b5563", fontFamily: "monospace",
                  lineHeight: 18 },
});

Step 7 — Run and test

npx expo start --clear

Tests

  1. Navigate to the ✝ Prayers tab. You should see:

    ✅  Database seeded
        2 prayers in database
    
    Prayers in database
      Ignatian Morning Prayer    2026-05-14
      Evening Examen             2026-05-14
    
  2. Restart test (no duplication).
    Stop the app, reopen it, and it should show 2 prayers — not 4

  3. Full reset test.
    Tap 🗑 Full reset + re-seed, confirm the alert. Restart the app → still 2 prayers.

Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.