Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse by CraigBuckmaster · Pull Request #1380 · CraigBuckmaster/ScriptureDeepDive · GitHub
Skip to content

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse - #1380

Merged
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync
Apr 16, 2026
Merged

fix(notifications): sync daily verse push with homepage VOTD + enable tap-to-verse#1380
CraigBuckmaster merged 7 commits into
masterfrom
fix/votd-notification-sync

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Problem

Bug #1 — Push notification VOTD doesn't match homepage VOTD.
The two code paths were completely independent:

  • Homepage (useHomeData.ts) → holiday override first, else deterministic pick from a curated 31-verse NIV FEATURED_VERSES list by day-of-year.
  • Notification (notifications.ts) → SELECT * FROM verses WHERE translation='kjv' ORDER BY RANDOM() LIMIT 1. Random. KJV. Unrelated.

Compounded by an iOS quirk: scheduled local notifications freeze their content at schedule time, so the old DAILY trigger fired the same random verse every morning forever until something rescheduled it.

Bug #2 — Tapping the notification didn't take you to the verse.
Two missing pieces:

  1. No tap handler at all — no addNotificationResponseReceivedListener anywhere in the codebase. Tapping just opened the app to the last state.
  2. Notification payload only carried { bookId, chapterNum } — no verseNum — so even if a handler existed, it couldn't route to a verse.

Bug #3 (bonus, discovered while investigating #2) — Scroll-to-verse had a silent layout race.
useChapterScroll.ts had the intent, but the effect ran when isLoading flipped to false — before onLayout callbacks populated verseYMap. Ref writes don't trigger re-renders, so the effect never retried. Net result: the deep-link verseNum parameter was effectively ignored on cold loads.

Architecture

FileChange
app/src/utils/verseOfDay.ts (new)Single source of truth. Exports getVerseForDate(date), getVerseOfDay(), getHolidayForDate(), getEncouragementForDate(), FEATURED_VERSES. Pure module — no DB/platform deps.
app/src/hooks/useHomeData.tsRefactored to consume the shared module. Homepage behaviour unchanged.
app/src/services/notifications.tsRewritten. Pre-schedules a rolling 60-day window of CALENDAR-triggered notifications, each with the correct verse for its date baked in (matches the homepage algorithm exactly). Uses scoped identifiers votd-YYYY-MM-DD so cancellation no longer nukes the reengagement-nudge slot. Payload now includes verseNum. New rescheduleIfStale() is idempotent and no-ops unless the last schedule was on a different calendar day.
app/src/hooks/useNotificationRouter.ts (new)Handles both cold-start taps (via getLastNotificationResponseAsync) and warm taps (via addNotificationResponseReceivedListener). Polls navRef.isReady() for up to 5s on cold start. Routes to HomeTab → Chapter with { bookId, chapterNum, verseNum }.
app/App.tsxAdded navigationRef, mounted useNotificationRouter in AppShell, wired rescheduleIfStale() into the foreground AppState handler.
app/src/hooks/chapter/useChapterScroll.tsFixed the layout race. Scroll is now a pending intent that gets flushed from both handleVerseLayout and handleSectionLayout — whichever completes the picture second triggers the scroll. Requires both section-y and verse-y to be known before flushing, guarding against the nested-onLayout-ordering edge case. Kept the effect-based fast-path for cached-chapter re-entry.

Why pre-schedule a rolling window?

iOS caps scheduled local notifications at 64 per app and freezes their content at schedule time. A single DAILY trigger cannot show a different verse each day without a backend push. Pre-scheduling 60 calendar-day-specific notifications:

  • Matches the homepage's deterministic verse per day exactly
  • Works fully offline
  • Leaves headroom (4 slots) for reengagement + any future scheduled features
  • Is refreshed opportunistically on app foreground (stale check: once per day at most)
  • Is scoped-cancellable so it doesn't interact with reengagement

Testing done

  • Syntax-parse check passes on all 6 files.
  • rescheduleIfStale() guards ensure it's cheap on steady state.
  • Scope-scoped cancellation (votd-* prefix) coexists with reengagement-nudge.
  • Tap payload narrowing is defensive — unknown shapes are ignored, not crashed on.

Testing needed (manual, on device)

  • Schedule a daily verse for ~2 minutes in the future, verify the verse matches the homepage VOTD for that day
  • Tap the notification cold — app should cold-start into the correct chapter + scroll to the correct verse
  • Tap the notification warm (app in background) — app should navigate + scroll
  • Verify reengagement notification still fires after 3 days of inactivity (scope-scoped cancellation didn't break it)
  • Open a chapter via any in-app verse reference — scroll should land on the referenced verse reliably (fixes the layout race for general deep links too)

Tier-2 accuracy impact

None — no content files touched.

@github-actions

github-actionsBot commented Apr 16, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3209❌ 03209
Suites✅ 429❌ 0429

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.2s

The notifications service was rewritten to use getVerseForDate() instead
of a DB query, scoped cancellation instead of cancelAll, and CALENDAR
triggers instead of DAILY. Update both test files to mock the new
dependencies (@/db/user, @/utils/verseOfDay) and assert against the new
behavior. Add missing expo-notifications mock entries (getAllScheduled,
cancelScheduled, CALENDAR) to jest.setup.js.
https://claude.ai/code/session_01Qj6otahNBTSak3fdYhFpes
@CraigBuckmaster
CraigBuckmaster merged commit 1ef9f88 into masterApr 16, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/votd-notification-sync branch April 16, 2026 16:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude