Skip to content

Add anonymous promos and PWA targeting system#256

Merged
kargig merged 2 commits into
mainfrom
feature/anonymous-user-promos
Jul 13, 2026
Merged

Add anonymous promos and PWA targeting system#256
kargig merged 2 commits into
mainfrom
feature/anonymous-user-promos

Conversation

@kargig

@kargig kargig commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Introduce a platform-aware promotion and PWA targeting manager system to gently encourage registrations and installations.

  • Track current session and lifetime cumulative page views to enforce "The Three-Page Rule", displaying promos only after active interest.
  • Implement a progressive dismissal gap mapped to absolute cumulative page-view targets (3 -> 7 -> 12 -> 18 -> 25) to prevent banner spam.
  • Set up PromoBannerManager globally to display sticky bottom sheets directing Android users to the Google Play Store app listing, floating bubble guides on iOS Safari, and sign-ups on Desktop.
  • Integrate compact inline promotional cards dynamically in lists of Dive Sites, public Dives, Diving Centers, and Dive Routes.
  • Inject cards using a modulo index check ((index - 2) % 15 === 0) to ensure they render regularly per "scroll page" on infinite scroll.
  • Customize promotional cards with distinct brand background styles, complete dark mode support, and a randomized pool of 10 feature CTAs.
  • Provide thorough Vitest unit testing to verify all state changes, logical boundaries, and layout overrides.

Introduce a platform-aware promotion and PWA targeting manager
system to gently encourage registrations and installations.

- Track current session and lifetime cumulative page views to enforce
  "The Three-Page Rule", displaying promos only after active interest.
- Implement a progressive dismissal gap mapped to absolute cumulative
  page-view targets (3 -> 7 -> 12 -> 18 -> 25) to prevent banner spam.
- Set up PromoBannerManager globally to display sticky bottom sheets
  directing Android users to the Google Play Store app listing,
  floating bubble guides on iOS Safari, and sign-ups on Desktop.
- Integrate compact inline promotional cards dynamically in lists of
  Dive Sites, public Dives, Diving Centers, and Dive Routes.
- Inject cards using a modulo index check ((index - 2) % 15 === 0)
  to ensure they render regularly per "scroll page" on infinite scroll.
- Customize promotional cards with distinct brand background styles,
  complete dark mode support, and a randomized pool of 10 feature CTAs.
- Provide thorough Vitest unit testing to verify all state changes,
  logical boundaries, and layout overrides.
@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

In dismissPromo(), the progressive target calculation uses Math.max(7, cumulative + 4) for the first dismissal. If the user dismisses the promo immediately at cumulative view 3, the next eligible target becomes Math.max(7, 7) = 7, which is correct. However, if the user dismisses at a later cumulative count (e.g., cumulative 25), the target becomes Math.max(7, 29) = 29, skipping the intended fixed thresholds (7, 12, 18, 25). This means the progressive gap logic does not enforce the absolute targets described in the spec (3 -> 7 -> 12 -> 18 -> 25) but instead uses relative offsets, which can lead to the promo never appearing again if dismissed late, or appearing at unexpected intervals.

if (dismissals === 1) target = Math.max(7, cumulative + 4);
else if (dismissals === 2) target = Math.max(12, cumulative + 5);
else if (dismissals === 3) target = Math.max(18, cumulative + 6);
else if (dismissals === 4) target = Math.max(25, cumulative + 7);
Possible Issue

The selectedPromo is recomputed on every render via useMemo with [promoPool] as dependency. Since promoPool is also memoized with [], the random index is only computed once per component mount. This is intentional to avoid re-shuffling on re-renders. However, the comment says "keep things fresh", but the random selection is deterministic per card mount and never changes. If the user scrolls through many pages of infinite scroll, the same promo message will appear repeatedly for each new card mount, which may feel stale rather than fresh.

const selectedPromo = useMemo(() => {
  const randomIndex = Math.floor(Math.random() * promoPool.length);
  return promoPool[randomIndex];
}, [promoPool]);
Accessibility Issue

The animate-pulse class is applied to the icon container on Android and iOS variants (lines 103 and 137). A pulsing animation can be distracting or trigger vestibular disorders in some users. Tailwind's animate-pulse is not disabled by prefers-reduced-motion. Consider adding motion-safe:animate-pulse to respect user accessibility preferences.

<div className='w-8 h-8 rounded-full bg-white dark:bg-gray-800 text-divemap-blue dark:text-divemap-sky flex items-center justify-center flex-shrink-0 shadow-sm animate-pulse'>

Address the requested quality, logic, and accessibility concerns
raised in the Pull Request review feedback.

- Refactor getPromoEligibility and dismissPromo to track absolute target
  view counts (3 -> 7 -> 12 -> 18 -> 25) using absolute TARGETS arrays.
- Support late dismissals gracefully by searching for the next absolute
  target strictly in the future, preventing unexpected instant triggers.
- Pass the map loop index to InFeedPromoCard and apply a deterministic
  modulo index check to rotate through 10 distinct feature highlights.
- Replace animate-pulse with motion-safe:animate-pulse to respect
  accessibility reduced-motion settings and prevent distractions.
- Ensure all 16 Vitest unit and mock test files pass completely.
@kargig
kargig merged commit 009433c into main Jul 13, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant