All things are difficult before they are easy, Thomas Fuller.
When you have eliminated the impossible, whatever remains, however improbable, must be the truth,Sherlock Holmes.

The app needs to fetch a “verse of the day” from an online source. We want three fallback mechanisms:
The caller (a custom hook, later) will try them in order. If all online sources fail (no network, all servers down), the hook falls back to an offline database. For that to work, each service function must never crash the app – it should simply return the verse or null.
If you’re coming from Python, this comparison makes the jump easier.
PYTHON requests REACT NATIVE fetch()
────────────────────────────────────── ──────────────────────────────────────
import requests Built-in, no import needed.
r = requests.get(url, timeout=10) const r = await fetch(url) (timeout is manual)
r.raise_for_status() if (!r.ok) throw new Error(r.status)
data = r.json() const data = await r.json()
text = r.text const text = await r.text()
# headers passed directly # headers in the options object
requests.get(url, headers={ fetch(url, {
"User-Agent": "myapp/1.0" headers: { "User-Agent": "myapp/1.0" }
}) })
The crucial difference: fetch() needs two awaits because the HTTP response is split into headers and body. The first await gets you the status code and headers; the second await reads the body stream.
Unlike Python where requests.get(url).json() is one operation, fetch() requires two awaits:
// Step 1: send the request and get the response headers + status
const response = await fetch(url);
// Step 2: read the response body
// (the body streams separately from the headers)
const data = await response.json(); // or response.text()
This separation is useful: you can inspect the status code before you try to parse the body, which is exactly what our service does.
Every public function in this module returns a union type: <Promise
export interface VerseResult {
ref: string; // The Bible reference string.
text: string; // The verse text.
source: string; // Human-readable description of the data source.
}
Because the functions never throw, callers can chain them with the nullish coalescing operator (??) without any try/catch as follows:
tsconst verse = await fetchOurManna() ?? await fetchYouVersion(key) ?? await fetchBibleApi("John 3:16");
// verse is VerseResult | null
This pattern makes control flow linear and predictable. We’ll use string | null later internally – for example, after extracting a string from the API response we might end up with an empty string, which we treat as null by falling through to the next check.
Unlike Python’s requests.get(url, timeout=6), fetch() has no built‑in timeout. If the server never responds, the promise will hang forever. React Native uses JavaScript engines that don’t kill hanging fetches automatically (though the app might eventually throw an AbortError if the OS closes the connection). We must implement our own timeout using the AbortController API.
// 1. Create an AbortController
const controller = new AbortController();
// This gives you a signal that can be passed into fetch().
// When you call controller.abort(), any fetch using that signal will immediately throw an error.
// 2. Start a timer, meaning: (i) If the fetch takes longer than timeoutMs, then the controller aborts
// and (ii) fetch throws an exception (AbortError).
const timeout = setTimeout(() => controller.abort(), 6000); // 6 seconds
try {
// 3. Perform the fetch with the abort signal
const response = await fetch(url, {
...options,
// AbortController.signal is passed to fetch.
// When abort() is called (after timeoutMs), the fetch promise rejects with an AbortError.
signal: controller.signal,
// We always set User-Agent and Accept headers.
// Some APIs refuse requests without a User-Agent, and Accept: application/json politely asks for JSON.
headers: {
"User-Agent": "PrayerCompanion/1.0",
"Accept": "application/json",
...((options.headers as Record<string, string>) ?? {}),
},
});
return response;
} catch (e) {
if (err.name === "AbortError") {
console.log("Request timed out");
} else {
console.log("Network error", err);
}
} finally {
// 4. Clean up the timer
clearTimeout(timer);
}
Equivalent Python: requests.get(url, timeout=6).
src/services/bibleService.ts// src/services/bibleService.ts
//
// ONLINE BIBLE API SERVICE
// ─────────────────────────
// Provides verse-of-the-day from three online sources.
// Each function is independent — if one fails, the caller tries the next.
//
// SOURCE PRIORITY:
// 1. fetchOurManna() — free, no key, OurManna.com
// 2. fetchYouVersion() — official API, requires YouVersion app key
// 3. fetchBibleApi(ref) — free, any reference, bible-api.com
//
// ERROR HANDLING PHILOSOPHY:
// Every function returns the result OR null.
// They NEVER throw — all errors are caught internally,
// logged to the Metro console, and the function returns null.
// The calling hook (useVerse) falls back to the offline KJV source.
//
// NETWORK POLICY:
// All requests have a 6-second timeout.
// Functions log errors to the Metro console for debugging.
import type { VerseResult } from "../db/types";
// ══════════════════════════════════════════════════════════════════════════════
// UTILITIES
// ══════════════════════════════════════════════════════════════════════════════
/**
* Create a fetch request with a timeout.
*
* fetch() does not have a built-in timeout parameter.
* AbortController lets us cancel the request after N milliseconds.
* A hung request can last forever (if the device loses Wi-Fi, switches from Wi-Fi to 4G, etc.)
*
* @param url The URL to fetch
* @param options Standard fetch options (headers, method, body)
* @param timeoutMs Milliseconds before the request is aborted (default 6000)
* @returns The Response object
* @throws AbortError if timed out, TypeError if network unreachable
*
* @example
* const r = await fetchWithTimeout("https://api.example.com/data", {}, 5000);
*/
async function fetchWithTimeout(
url: string,
options: RequestInit = {},
timeoutMs: number = 6000
): Promise<Response> {
// 1. Create an AbortController. This gives you **a signal object** that can be passed into fetch().
// Think of it as a “kill switch” for the fetch.
// When you call controller.abort(), any fetch using that signal will immediately throw an error.
const controller = new AbortController();
// 2. Start a timer, meaning: (i) If the fetch takes longer than timeoutMs, then the controller aborts
// and (ii) fetch throws an exception (AbortError).
const timer = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
// 3. Perform the fetch with the abort signal
const response = await fetch(url, {
...options,
// AbortController.signal is passed to fetch.
// When abort() is called (after timeoutMs), the fetch promise rejects with an AbortError.
signal: controller.signal,
headers: {
// We always set User-Agent and Accept headers.
// Some APIs refuse requests without a User-Agent, and Accept: application/json politely asks for JSON.
// We preserve user headers while enforcing our own defaults.
"User-Agent": "PrayerCompanion/1.0",
"Accept": "application/json",
...((options.headers as Record<string, string>) ?? {}),
},
});
return response;
} finally {
/* 4. Always clear the timeout.
If fetch succeeds → you don’t want the timeout firing later
If fetch fails or aborts → you still don’t want the timeout firing */
clearTimeout(timer);
}
}
/**
* Decode common HTML entities in a string.
*
* Bible API responses sometimes contain HTML entities like `—` (—), `’` (’), or `&` (&).
* React Native's <Text> component displays these as literal text (raw characters) unless
* we decode them first before storing the verse.
*
* For a production app you might use the he library (npm install he),
* but this implementation covers the API responses we’ve encountered without an extra dependency.
*/
function decodeHtmlEntities(text: string): string {
return text
.replace(/—/g, "—")
.replace(/–/g, "–")
.replace(/‘/g, "\u2018") // left single quote
.replace(/’/g, "\u2019") // left single quote
.replace(/“/g, "\u201C") // left double quote
.replace(/”/g, "\u201D") // right double quote
.replace(/&/g, "&")
.replace(/ /g, " ")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
.replace(/\s+/g, " ") // collapse multiple spaces/newlines
.trim();
}
/**
* Safely parse a JSON response.
* Returns null if the body is not valid JSON (avoids uncaught parse errors).
* Now we can call safeJson(r) and if the API returns garbage,
* we get null instead of a broken promise. */
async function safeJson(response: Response): Promise<unknown> {
try {
// The response.json() method will throw if the body isn’t valid JSON.
return await response.json();
} catch {
// Since we never want an unhandled error to crash the service, we wrap it.
return null;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// SOURCE 1: OUR MANNA (free, no API key required)
// ══════════════════════════════════════════════════════════════════════════════
/**
* Fetch the verse of the day from OurManna.com.
*
* OurManna provides a free, unauthenticated daily verse API.
* The response updates once per day at midnight UTC.
*
* API endpoint:
* GET https://beta.ourmanna.com/api/v1/get?format=json&order=daily
*
* API response shape:
* {
* "verse": {
* "details": {
* "text": "To all perfection I see a limit...",
* "reference": "Psalm 119:96",
* "version": "NIV",
* "verseurl": "http://www.ourmanna.com/"
* },
* "notice": "Powered by OurManna.com"
* }
* }
*
* @returns VerseResult with ref, text, source — or null on any error
*/
export async function fetchOurManna(): Promise<VerseResult | null> {
const url =
"https://beta.ourmanna.com/api/v1/get?format=json&order=daily";
try {
// 1. We call our timeout‑wrapped fetch. If the network is unreachable or the timeout fires,
// this will throw – but the outer try will catch it and return null.
const response = await fetchWithTimeout(url);
// 2. If the server responds with a 4xx or 5xx status, response.ok is false.
// We log it (for debugging) and bail out.
if (!response.ok) {
console.warn(`[Bible] OurManna returned HTTP ${response.status}`);
return null;
}
// 3. safeJson returns the parsed object (unknown) or null (on failure).
// We then confirm it’s actually an object.
const data = await safeJson(response);
if (!data || typeof data !== "object") {
console.warn("[Bible] OurManna returned non-JSON body");
return null;
}
// At this point, TypeScript still doesn’t know what kind of object it is.
// It only knows: it’s not null; it’s not a primitive; it’s some object with unknown keys.
// 4. Navigate the nested structure safely using optional chaining (?.)
// instead of repeated null checks.
// First, we need to tell TypeScript: This is an object with string keys, and I don’t care about the value types.
// That’s exactly what Record<string, any> means.
// If verse or details is missing, the whole expression becomes undefined instead of throwing.
const details = (data as Record<string, any>)?.verse?.details;
if (!details) {
console.warn("[Bible] OurManna: unexpected JSON structure", data);
return null;
}
// 5. We decode entities in both the text and the reference,
// then verify we got non‑empty strings. If something is missing, we return null.
const text = decodeHtmlEntities(String(details.text ?? "").trim());
const ref = decodeHtmlEntities(String(details.reference ?? "").trim());
if (!text || !ref) {
console.warn("[Bible] OurManna: missing text or reference");
return null;
}
// 6. Finally, we log a success message and return a VerseResult.
// The source field is a human‑readable string like "OurManna.com · NIV".
// The trailing regex cleanup removes a hanging · if there’s no version.
console.log(`[Bible] OurManna: ${ref}`);
return {
ref,
text,
source: `OurManna.com · ${details.version ?? ""}`.trim().replace(/·\s*$/, ""),
};
} catch (e: unknown) {
// AbortError = timed out. TypeError = network unreachable.
// Both are expected during normal use (offline, slow network).
const name = (e as Error).name;
// We differentiate between a timeout and other network errors only for logging purposes
// – the caller gets null either way.
if (name === "AbortError") {
console.warn("[Bible] OurManna timed out.");
} else {
console.warn("[Bible] OurManna error:", (e as Error).message);
}
return null;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// SOURCE 2: YOUVERSION (requires API key)
// ══════════════════════════════════════════════════════════════════════════════
/**
* Fetch today's verse from YouVersion's official API.
*
* **Prerequisites**
* YouVersion (Bible.com) requires an API key stored in the password store.
* Register at
* {@link https://developer.youversion.com/} to obtain one.
*
* HOW TO STORE YOUR KEY:
* The key must be stored securely. In this app it lives in
* `expo-secure-store` after being entered via the Settings screen.
*
* API ENDPOINTS:
* Retrieve the verse ID for today's date from
* GET /v1/verse_of_the_days/{day_of_year}
* Response: { "verse_of_the_day": { "day": 134, "verse": { "human_ref": "John 3:16", ... } } }
*
* Use that ID to fetch the full verse text from
* GET /v1/verses/{verse_id}?version_id=1 (version_id 1 = KJV)
* Response: { "verse": { "text": "...", "human_ref": "..." } }
*
* **Parameters**
* @param apiKey Your YouVersion API key
* @param dayOfYear Day of year (1-366). When omitted, today is calculated
* in the local timezone using `getDayOfYear()`.
* @returns VerseResult or null.
* A `VerseResult` object containing `ref` (human reference), `text` (verse content),
* and `source` (always `"YouVersion (KJV)"`). Returns `null` if:
* - No API key is provided.
* - The first API call fails or doesn't contain a verse ID.
* - The second API call fails or returns incomplete data.
* - The request times out (handled by `fetchWithTimeout`).
*/
export async function fetchYouVersion(
apiKey: string,
dayOfYear?: number
): Promise<VerseResult | null> {
if (!apiKey?.trim()) {
console.warn("[Bible] YouVersion: no API key provided.");
return null;
}
// Use current local day if not supplied
const day = dayOfYear ?? getDayOfYear();
if (day < 1 || day > 366) {
console.warn(`[Bible] YouVersion: invalid dayOfYear ${day}`);
return null;
}
// ⚠️ Verify the header name with your developer portal
const headers = { "X-YouVersion-Token": apiKey.trim() };
try {
// Step 1: get the verse ID for today
const votdUrl = `https://api.youversion.com/v1/verse_of_the_days/${day}`;
// Add a signal if using AbortController for more granular control
const votdResponse = await fetchWithTimeout(votdUrl, { headers });
if (!votdResponse.ok) {
console.warn(`[Bible] YouVersion /verse_of_the_days returned ${votdResponse.status}`);
return null;
}
const votdData = await safeJson(votdResponse) as Record<string, any>;
const verseId = votdData?.verse_of_the_day?.verse?.id;
if (!verseId) {
console.warn("[Bible] YouVersion: no verse ID in response", votdData);
return null;
}
// Step 2: get the actual verse text
// version_id=1 → King James Version. See YouVersion docs for others.
const verseUrl = `https://api.youversion.com/v1/verses/${verseId}?version_id=1`;
const verseResponse = await fetchWithTimeout(verseUrl, { headers });
if (!verseResponse.ok) {
console.warn(`[Bible] YouVersion /verses returned ${verseResponse.status}`);
return null;
}
const verseData = await safeJson(verseResponse) as Record<string, any>;
const text = decodeHtmlEntities(String(verseData?.verse?.text ?? "").trim());
const ref = decodeHtmlEntities(String(verseData?.verse?.human_ref ?? "").trim());
if (!verseData) {
console.warn("[Bible] YouVersion: empty or invalid JSON from verses endpoint");
return null;
}
if (!text || !ref) {
console.warn("[Bible] YouVersion: missing text or reference");
return null;
}
console.log(`[Bible] YouVersion: ${ref}`);
return { ref, text, source: "YouVersion (KJV)" };
} catch (e: unknown) {
const name = (e as Error).name;
if (name === "AbortError") {
console.warn("[Bible] YouVersion timed out.");
} else {
console.warn("[Bible] YouVersion error:", (e as Error).message);
}
return null;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// SOURCE 3: BIBLE-API.COM (free, look up any reference)
// ══════════════════════════════════════════════════════════════════════════════
/**
* Retrieve a specific Bible verse or passage from the free bible-api.com service.
*
* **Purpose**
* This is a **fallback source** — it is not a “verse of the day” API. It
* looks up an exact reference, such as `"John 3:16"` or `"Psalm 23:1-3"`.
* It is used when a daily reading plan provides a reference that cannot be
* resolved via the offline KJV database (e.g., because the freebible module
* is not installed or the reference is missing).
*
* * **API details**
* - Endpoint: `GET https://bible-api.com/{reference}?translation=kjv`
* - The `reference` path segment must be URL‑encoded (spaces become `%20`).
* - This service is **free** and **rate‑limited** (aggressive polling may
* result in HTTP 429 responses). Consider adding local caching and a
* retry‑with‑backoff mechanism for production use.
*
* API response shape:
* {
* "reference": "John 3:16",
* "text": ""For God so loved the world, that he gave his only begotten Son...",
* "translation_name": "King James Version",
* "verses": [
* { "book_id": "JHN",
* "chapter": 3,
* "verse": 16,
* "text": "For God so loved the world, that he gave his only begotten So..." }
* ]
* }
*
* The `text` field contains the full passage with verse numbers embedded
* (e.g., `16 For God so loved…`). This function returns only the
* plain‑text content after decoding any HTML entities.
*
* @param reference A human-readable reference, e.g. "John 3:16" or
* "Psalm 23:1-3". Leading/trailing spaces are trimmed.
* @returns The verse text as a plain string, or null if the lookup fails.
*
* @example
* const verseText = await fetchBibleApi("1 Corinthians 13:4-7");
* if (verseText) {
* console.log(verseText);
* } else {
* console.log("Verse not found or network error");
* }
*/
export async function fetchBibleApi(reference: string): Promise<string | null> {
if (!reference?.trim()) {
console.warn("[Bible] bible-api.com: empty reference");
return null;
}
// URL-encode the reference: "John 3:16" → "John%203:16"
// The colon (:) does not need encoding but the space does (spaces → %20).
const encoded = encodeURIComponent(reference.trim());
const url = `https://bible-api.com/${encoded}?translation=kjv`;
try {
/* A more sophisticated version would be:
const response = await fetchWithTimeout(url, {
headers: { Accept: "application/json" },
}); */
const response = await fetchWithTimeout(url);
// The API returns HTTP 404 for unknown references and 429 for rate limits
if (!response.ok) {
console.warn(`[Bible] bible-api.com returned HTTP ${response.status} for "${reference}"`);
return null;
}
// It calls (and **parse JSON defensively**) response.json(). So data is either:
// a parsed JSON object, or null if the server returned invalid JSON.
// Record<string, any> means: “This is an object with string keys, but I don’t know the exact structure.”
// Without the assertion, TypeScript would complain when you do data?.text because safeJson might return null.
const data = await safeJson(response) as Record<string, any>;
// This line **extracts the text field defensively**.
// data?.text: If data is null, this becomes undefined. If data exists but has no text field, also undefined.
// ?? "" Nullish coalescing: If the left side is null or undefined, use "".
// Prevents crashes and guarantees a string-like value.
// String(...). Wraps the result in String() to ensure the final value is a real string.
// decodeHtmlEntities(...) is the final cleanup step.
// Many APIs return HTML‑encoded text, so this helper converts those into real characters.
const text = decodeHtmlEntities(String(data?.text ?? "").trim());
if (!text) {
console.warn(`[Bible] bible-api.com: no text for "${reference}"`, data);
return null;
}
console.log(`[Bible] bible-api.com: ${reference} (${text.length} chars)`);
return text;
} catch (e: unknown) {
const name = (e as Error).name;
if (name === "AbortError") {
console.warn(`[Bible] bible-api.com timed out for "${reference}".`);
} else {
console.warn(`[Bible] bible-api.com error for "${reference}":`, (e as Error).message);
}
return null;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// HELPER: day of year
// ══════════════════════════════════════════════════════════════════════════════
/**
* Return today's ordinal day (1‑366) in the **local** timezone.
* Local helper so bibleService.ts does not need to import dateUtils.ts.
* The dateUtils import is used in other files. This is intentionally kept inline to avoid circular dependencies.
*
* @returns An integer between 1 and 366 representing today's day of the year.
*/
function getDayOfYear(): number {
const now = new Date();
const jan1 = new Date(now.getFullYear(), 0, 1);
// Day count: floor((now - start) / ms_per_day) + 1
return Math.floor((now.getTime() - jan1.getTime()) / 86_400_000) + 1;
}
In this step we will add a diagnostic panel below the existing date picker on the Verse tab. The panel executes two live API calls —fetchOurManna() (OurManna VOTD) and fetchBibleApi("John 3:16") (bible-api.com) —and displays their results in real time. This serves as a developer tool to verify that the network layer, API keys, and parsing logic are working correctly during development.
The panel:
Add to the imports at the top of app/(tabs)/verse.tsx:
// Prerequisites. Open app/(tabs)/verse.tsx and add the following imports at the top of the file.
// fetchOurManna is our main verse-of-the-day function.
// fetchBibleApi is a reference lookup fallback.
// VerseResult is the TypeScript type describing the shape of the OurManna result.
import { fetchOurManna, fetchBibleApi } from "../../src/services/bibleService";
import type { VerseResult } from "../../src/db/types";
Add this component at the bottom of verse.tsx, after dateStyles:
// ── Add the ApiVersePanel Component ────────────────────────────────
/* ApiVersePanel is a diagnostic UI widget we drop into verse.tsx
so we can see what our two online providers return, fetchOurManna, and fetchBibleApi. */
function ApiVersePanel() {
/* Initialises two state variables, ourmanna and bibleApi
Each state variable can be: "loading" → initial state; null → API failed or offline;
VerseResult or string → success. */
const [ourmanna, setOurmanna] = useState<VerseResult | null | "loading">("loading");
const [bibleApi, setBibleApi] = useState<string | null | "loading">("loading");
// Runs both API calls in a useEffect with an empty dependency array.
// Empty dependency array → runs once when the component mounts.
// Each fetch returns a Promise.
// OurManna (fetchOurManna): Requests the verse of the day from the OurManna API.
// Returns a VerseResult object ({ ref, text, source }) on success.
// If the network fails or the response is invalid, it returns null (and logs a warning).
// Bible‑API (fetchBibleApi): Fetches the King James Version text for a specific reference (here "John 3:16").
// Returns the plain verse text as a string, or null if anything goes wrong.
useEffect(() => {
/* When we call the function fetchOurManna(): async function fetchOurManna(): Promise<VerseResult | null> { ... }
we immediately get a Promise that will eventually resolve to (i) a VerseResult object or (ii) null (offline/error)
.then(setOurmanna) is shorthand for:
fetchOurManna().then((result) => {
setOurmanna(result);
});
When the Promise resolves, React calls our state setter (setOurmanna) with the resolved value.
Case A — Success: fetchOurManna() resolves to { ref: "John 3:16", text: "...", source: "OurManna.com · NIV" }
React calls: setOurmanna({ ref, text, source }); UI re-renders and shows the verse.
Case B — Failure: fetchOurManna() returns: null.
React calls: setOurmanna(null); UI re-renders and shows: ❌ null (offline or API error)*/
fetchOurManna().then(setOurmanna);
fetchBibleApi("John 3:16").then(setBibleApi);
}, []);
// A helper renderResult decides what to display based on the value’s type and state:
const renderResult = (
label: string,
value: VerseResult | string | null | "loading",
) => (
<View style={apiStyles.resultBlock}>
<Text style={apiStyles.resultLabel}>{label}</Text>
// "loading" → ActivityIndicator (spinner)
{value === "loading" ? (
<ActivityIndicator color="#c9a84c" size="small" />
// null → red error message
) : value === null ? (
<Text style={apiStyles.resultError}>❌ null (offline or API error)</Text>
// string → first 120 characters of the verse text (for bible‑api.com)
) : typeof value === "string" ? (
<Text style={apiStyles.resultText}>{value.slice(0, 120)}…</Text>
// VerseResult → reference, text snippet, and source name
) : (
<>
<Text style={apiStyles.resultRef}>{value.ref}</Text>
<Text style={apiStyles.resultText}>{value.text.slice(0, 120)}…</Text>
<Text style={apiStyles.resultSource}>{value.source}</Text>
>
)}
</View>
);
/* Component output: You get a card with: (i) Title; (ii) One block for each API
(iii) A note reminding you to check Metro logs. */
return (
<View style={apiStyles.card}>
<Text style={apiStyles.title}>🌐 Online Bible APIs</Text>
{renderResult("fetchOurManna()", ourmanna)}
{renderResult('fetchBibleApi("John 3:16")', bibleApi)}
<Text style={apiStyles.note}>
Check Metro log for [Bible] entries.{"\n"}
null = network error or offline (expected without internet).
</Text>
</View>
);
}
const apiStyles = StyleSheet.create({
card: { backgroundColor: "#222840", borderRadius: 10, padding: 16,
marginBottom: 16, borderWidth: 1, borderColor: "#2d3550" },
title: { fontSize: 15, fontWeight: "bold", color: "#c9a84c",
marginBottom: 14 },
resultBlock: { marginBottom: 14, gap: 4 },
resultLabel: { fontSize: 11, color: "#6b7280", fontFamily: "monospace",
marginBottom: 2 },
resultRef: { fontSize: 14, color: "#c9a84c", fontWeight: "600" },
resultText: { fontSize: 13, color: "#dde4f0", lineHeight: 20 },
resultSource: { fontSize: 11, color: "#5b8fd4" },
resultError: { fontSize: 13, color: "#f87171" },
note: { fontSize: 11, color: "#4b5563", marginTop: 4,
lineHeight: 16, fontStyle: "italic" },
});
Inside the VerseScreen component, locate the that contains the component.
{/* Immediately after , add: */}
<ApiVersePanel />
npx expo start --clearfetchOurManna()
❌ null (offline or API error)
fetchBibleApi("John 3:16")
❌ null (offline or API error)