Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apis/api-users/src/schema/user/findOrFetchUser.ts
Copy link
Copy Markdown
Contributor

@Ur-imazing Ur-imazing Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't seem to be related to the existing issue. Moreover, since this is a back-end change, please merge into production as a separate PR first, before merging in the remaining front-end changes.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function findOrFetchUser(
if (existingUser != null && existingUser.emailVerified == null) {
const user = await prisma.user.update({
where: {
id: userId
userId
},
data: {
emailVerified: false
Expand Down Expand Up @@ -86,7 +86,7 @@ export async function findOrFetchUser(
do {
user = await prisma.user.update({
where: {
id: userId
userId
},
data
})
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add unit tests for the changes made to this component.

Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Box from '@mui/material/Box'
import { styled } from '@mui/material/styles'
import { CSSProperties, ReactElement, useEffect, useRef } from 'react'
import { CSSProperties, ReactElement, useEffect, useRef, useState } from 'react'
import videojs from 'video.js'
import Player from 'video.js/dist/types/player'

import { isInstagramAndroidWebView } from '@core/shared/ui/deviceUtils'
import { defaultBackgroundVideoJsOptions } from '@core/shared/ui/defaultVideoJsOptions'

import {
Expand Down Expand Up @@ -36,6 +37,11 @@ export function BackgroundVideo({
const videoRef = useRef<HTMLVideoElement>(null)
const playerRef = useRef<Player | null>(null)
const isYouTube = source === VideoBlockSource.youTube
const [inAppBrowser, setInAppBrowser] = useState(false)

useEffect(() => {
setInAppBrowser(isInstagramAndroidWebView())
}, [])
Comment on lines +40 to +44
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: video.js player is initialized before the iframe path takes over, leaving it orphaned and setLoading unresolved.

All useEffect hooks with [] deps run sequentially after the first commit. On that first render inAppBrowser is still false, so StyledVideo is in the DOM and videoRef.current is valid. The execution order is:

  1. Effect at lines 42–44 → setInAppBrowser(true) (schedules a state update)
  2. Effect at lines 47–68 → videojs(videoRef.current, …) runs ✅ — player IS created
  3. Effect at lines 71–124 → event handlers attached ('seeked'setLoading(false))
  4. State flush → inAppBrowser = true → re-render → StyledVideo removed

Consequences:

  • The orphaned player is never disposed, causing a memory leak and unnecessary network requests to YouTube (which fail in Instagram's WebView anyway).
  • setLoading(false) is wired to the player's 'seeked' event. Because YouTube never loads in Instagram WebView, seeked never fires, so setLoading stays true and the parent's loading spinner can be stuck indefinitely.

The inAppBrowser iframe path should also call setLoading(false) on mount, and the player init effect should be skipped when in-app-browser is active.

🛠️ Suggested fix

Replace the useState(false) + useEffect pair with a lazy initializer so the value is known at first render (avoiding the player-init race), and add setLoading(false) in the iframe branch:

-  const [inAppBrowser, setInAppBrowser] = useState(false)
-
-  useEffect(() => {
-    setInAppBrowser(isInstagramAndroidWebView())
-  }, [])
+  const [inAppBrowser] = useState<boolean>(
+    () => typeof navigator !== 'undefined' && isInstagramAndroidWebView()
+  )

   // Initiate Video
   useEffect(() => {
-    if (videoRef.current != null) {
+    if (!inAppBrowser && videoRef.current != null) {
       playerRef.current = videojs(videoRef.current, {
         ...
       })
     }
-  }, [])
+  }, [inAppBrowser])

And in the iframe JSX branch, resolve the loading state immediately:

   if (inAppBrowser && isYouTube && videoId != null) {
+    setLoading(false)  // or use a one-time useEffect in a sub-component
     return (
       <Box ...>
         <iframe ... />
       </Box>
     )
   }

⚠️ Note: the lazy initializer computes false on the server (SSR) and the actual value on the client — if the component is SSR'd, React may log a hydration mismatch. Given this is a mobile in-app browser path, that trade-off is usually acceptable; alternatively keep the useEffect approach but add an explicit dispose() cleanup on the player and the iframe-branch useEffect for setLoading(false).

Also applies to: 47-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx`
around lines 40 - 44, Replace the current useState + useEffect that sets
inAppBrowser with a lazy initializer so inAppBrowser is correctly known on first
render (useState(() => isInstagramAndroidWebView())). Then update the player
initialization effect (the effect that calls videojs with videoRef.current and
attaches 'seeked' => setLoading(false)) to early-return when inAppBrowser is
true so the player is not created for the iframe path; finally, in the iframe
branch (where StyledVideo is not rendered) call setLoading(false) on mount
(e.g., a small effect tied to the iframe branch) so loading is resolved
immediately; also ensure any created player is disposed in the player cleanup to
avoid orphaned players if state changes.


// Initiate Video
useEffect(() => {
Expand Down Expand Up @@ -136,6 +142,26 @@ export function BackgroundVideo({
}
}

if (inAppBrowser && isYouTube && videoId != null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets rename for clarity, maybe something like this.

Suggested change
if (inAppBrowser && isYouTube && videoId != null) {
if ( useIframePlayer && isYouTube && videoId != null) {

Also make the same change for Video.tsx

return (
<Box
height="100%"
width="100%"
minHeight="-webkit-fill-available"
overflow="hidden"
position="absolute"
data-testid="CardContainedBackgroundVideo"
>
<iframe
src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playsinline=1&controls=0`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

YouTube IFrame embed API end parameter behavior documentation

💡 Result:

The YouTube IFrame embed end player parameter (passed in the embed URL query string or in playerVars) is documented as:

  • Meaning: “the time, measured in seconds from the start of the video, when the player should stop playing the video.”
  • Type: positive integer.
  • Important behavior: the stop time is measured from the beginning of the video, not relative to the start parameter (and not relative to the startSeconds used in API load/queue calls). [1]

Related (API-call) behavior that’s often confused with end:

  • In the IFrame Player API methods like loadVideoById({ ..., startSeconds, endSeconds }), endSeconds sets the stop time for that playback call, but calling seekTo() cancels the endSeconds limit (it “will no longer be in effect”). [2]

Sources:
[1] YouTube “Embedded Players and Player Parameters” (end parameter) https://developers.google.com/youtube/player_parameters
[2] YouTube “IFrame Player API Reference” (endSeconds in loadVideoById) https://developers.google.com/youtube/iframe_api_reference


🏁 Script executed:

# Let's examine the actual code file and its context
fd BackgroundVideo.tsx --type f

Repository: JesusFilm/core

Length of output: 145


🏁 Script executed:

# Read the BackgroundVideo.tsx file to see the context
cat -n libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx | sed -n '140,170p'

Repository: JesusFilm/core

Length of output: 1057


🏁 Script executed:

# Look at the beginning of the file to find where startAt and endAt are defined
head -n 145 libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx | tail -n 100

Repository: JesusFilm/core

Length of output: 2739


🏁 Script executed:

# Search for startAt and endAt declarations in the file
rg -n "startAt|endAt" libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx

Repository: JesusFilm/core

Length of output: 671


🏁 Script executed:

# Search for videojs usage mentioned in the review comment
rg -n "videojs" libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx

Repository: JesusFilm/core

Length of output: 175


🏁 Script executed:

# Let me see what's at line 219 (another endAt issue)
sed -n '210,230p' libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx | cat -n

Repository: JesusFilm/core

Length of output: 957


🏁 Script executed:

# Get the full component signature to understand startAt and endAt types
head -n 50 libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx

Repository: JesusFilm/core

Length of output: 1558


Fix YouTube iframe end parameter defaulting to 0 which stops playback.

When endAt is null or undefined, the current code defaults to end=0 in the iframe URL. According to YouTube's IFrame embed API documentation, the end parameter specifies "the time, measured in seconds from the start of the video, when the player should stop playing the video." Setting end=0 causes the player to stop at the very beginning, preventing playback entirely.

The fix should conditionally include the end parameter only when endAt is defined:

Proposed fix for line 156
-          src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playsinline=1&controls=0`}
+          src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}${endAt != null ? `&end=${endAt}` : ''}&autoplay=1&mute=1&loop=1&playsinline=1&controls=0`}

Note: The same issue exists at line 219 in the videojs-youtube source tag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx`
at line 156, The iframe src construction in BackgroundVideo is adding end=0 when
endAt is undefined which stops playback; update the src string building logic in
the BackgroundVideo component (where
src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt
?? 0}&...` } is created) to only append the &end=... query parameter when endAt
is not null/undefined (e.g., conditionally add `&end=${endAt}` only if endAt !=
null), leaving the end parameter out otherwise; apply the same
conditional-append fix to the videojs-youtube source tag construction that
currently uses endAt similarly.

allow="accelerometer; autoplay; encrypted-media"
style={{ width: '100%', height: '100%', border: 'none', pointerEvents: 'none' }}
title="Background video"
/>
Comment on lines +155 to +160
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

loop=1 without &playlist=${videoId} — the video will not loop.

The YouTube embed loop parameter only works in conjunction with the playlist parameter. To loop a single video, set loop=1 and playlist=VIDEO_ID in the embed URL. Without playlist, the video plays once and stops — the background will be a frozen last frame.

🛠️ Proposed fix
-          src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playsinline=1&controls=0`}
+          src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playlist=${videoId}&playsinline=1&controls=0`}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<iframe
src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playsinline=1&controls=0`}
allow="accelerometer; autoplay; encrypted-media"
style={{ width: '100%', height: '100%', border: 'none', pointerEvents: 'none' }}
title="Background video"
/>
<iframe
src={`https://www.youtube.com/embed/${videoId}?start=${startAt ?? 0}&end=${endAt ?? 0}&autoplay=1&mute=1&loop=1&playlist=${videoId}&playsinline=1&controls=0`}
allow="accelerometer; autoplay; encrypted-media"
style={{ width: '100%', height: '100%', border: 'none', pointerEvents: 'none' }}
title="Background video"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx`
around lines 155 - 160, The iframe embed in BackgroundVideo uses loop=1 but
omits the required playlist parameter, so the video won't actually loop; update
the iframe src construction in BackgroundVideo (the template that builds the
YouTube URL for the iframe) to append &playlist=${videoId} whenever loop=1 (or
always append playlist=${videoId} when videoId is present) so the embed URL
becomes
...?start=...&end=...&autoplay=1&mute=1&loop=1&playlist=${videoId}&playsinline=1&controls=0;
ensure videoId is used safely (only add playlist if videoId exists) and keep
pointerEvents/style/title unchanged.

</Box>
)
}

return (
<Box
height="100%"
Expand Down
25 changes: 24 additions & 1 deletion libs/journeys/ui/src/components/Video/Video.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add unit tests for the changes made to this component.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from 'react'
import { use100vh } from 'react-div-100vh'

import { isInstagramAndroidWebView } from '@core/shared/ui/deviceUtils'
import { NextImage } from '@core/shared/ui/NextImage'

import {
Expand Down Expand Up @@ -74,6 +75,11 @@ export function Video({
const [player, setPlayer] = useState<VideoJsPlayer>()
const [showPoster, setShowPoster] = useState(true)
const [activeStep, setActiveStep] = useState(false)
const [inAppBrowser, setInAppBrowser] = useState(false)

useEffect(() => {
setInAppBrowser(isInstagramAndroidWebView())
}, [])
Comment on lines +78 to +82
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate InitAndPlay component
fd "InitAndPlay" --type f -e ts -e tsx

Repository: JesusFilm/core

Length of output: 194


🏁 Script executed:

#!/bin/bash
# Read InitAndPlay implementation
cat -n libs/journeys/ui/src/components/Video/InitAndPlay/InitAndPlay.tsx

Repository: JesusFilm/core

Length of output: 10452


🏁 Script executed:

#!/bin/bash
# Check Video.tsx for any cleanup logic and VideoEvents implementation
cat -n libs/journeys/ui/src/components/Video/Video.tsx | head -250

Repository: JesusFilm/core

Length of output: 9153


🏁 Script executed:

#!/bin/bash
# Continue reading Video.tsx from line 250 onwards
tail -n +250 libs/journeys/ui/src/components/Video/Video.tsx

Repository: JesusFilm/core

Length of output: 4820


🏁 Script executed:

#!/bin/bash
# Check if there's any cleanup logic when videoRef changes or when player is disposed
grep -n "dispose\|cleanup\|unmount" libs/journeys/ui/src/components/Video/Video.tsx

Repository: JesusFilm/core

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Find and inspect VideoEvents implementation
fd "VideoEvents" --type f -e ts -e tsx | head -5

Repository: JesusFilm/core

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Read VideoEvents implementation
cat -n libs/journeys/ui/src/components/VideoEvents/VideoEvents.tsx

Repository: JesusFilm/core

Length of output: 27171


🏁 Script executed:

#!/bin/bash
# Check if player is disposed/cleaned up when inAppBrowser changes
grep -rn "dispose\|destroy" libs/journeys/ui/src/components/Video/ --include="*.tsx" --include="*.ts"

Repository: JesusFilm/core

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Check if player is conditionally rendered or cleaned up anywhere
grep -n "setPlayer\|player =" libs/journeys/ui/src/components/Video/Video.tsx

Repository: JesusFilm/core

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Check BackgroundVideo.tsx to see if same pattern exists there for comparison
fd "BackgroundVideo" --type f -e tsx

Repository: JesusFilm/core

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Read BackgroundVideo.tsx to compare the pattern
cat -n libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx | head -150

Repository: JesusFilm/core

Length of output: 5226


🏁 Script executed:

#!/bin/bash
# Continue reading BackgroundVideo.tsx to see the full conditional rendering
tail -n +145 libs/journeys/ui/src/components/Card/ContainedCover/BackgroundVideo/BackgroundVideo.tsx

Repository: JesusFilm/core

Length of output: 2710


🏁 Script executed:

#!/bin/bash
# Check if there's any cleanup when player or inAppBrowser changes in Video.tsx
grep -A5 -B5 "useEffect.*inAppBrowser\|useEffect.*player\|setPlayer.*null" libs/journeys/ui/src/components/Video/Video.tsx

Repository: JesusFilm/core

Length of output: 40


Conditional InitAndPlay and player cleanup needed—same race condition as BackgroundVideo.tsx.

InitAndPlay is always rendered (line 187) and initializes the video.js player on first render, before inAppBrowser resolves from its async check. When inAppBrowser becomes true:

  1. StyledVideo is removed from the DOM; videoRef.current becomes null
  2. InitAndPlay remains mounted with a player object that has no backing DOM element
  3. VideoEvents (lines 211–227) still renders if activeStep && player != null, attaching listeners to the orphaned player
  4. Any player events fire analytics mutations and GTM events for a video that isn't actually displayed

Fix: Make InitAndPlay conditional on !inAppBrowser, or apply the player-initialization safeguard used in BackgroundVideo.tsx (use playerRef with a guard to prevent initialization if the condition changes mid-render).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Video/Video.tsx` around lines 78 - 82,
InitAndPlay currently always initializes the video.js player before the async
isInstagramAndroidWebView check completes, causing an orphaned player when
StyledVideo/videoRef is removed; update the component so InitAndPlay only
initializes when the inAppBrowser flag is false (i.e., render InitAndPlay
conditionally: {!inAppBrowser && <InitAndPlay ...>}), or implement the same
playerRef guard pattern used in BackgroundVideo.tsx (check videoRef.current
before creating/attaching the player in InitAndPlay, store the instance in
playerRef, and ensure cleanup/dispose and removal of VideoEvents listeners when
videoRef becomes null) so VideoEvents (which attaches listeners to player) never
binds to an orphaned player.


const { blockHistory } = useBlocks()
const { variant, journey } = useJourney()
Expand Down Expand Up @@ -220,7 +226,24 @@ export function Video({
/>
)}

{videoId != null ? (
{videoId != null && inAppBrowser && source === VideoBlockSource.youTube ? (
<Box
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the iframe player has different styling on the box, was this intentional?

data-testid="in-app-browser-youtube-fallback"
sx={{
width: '100%',
height: '100%',
position: 'relative'
}}
>
<iframe
src={`https://www.youtube.com/embed/${videoId}?start=${effectiveStartAt}&end=${effectiveEndAt}&autoplay=0&playsinline=1`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowFullScreen
style={{ width: '100%', height: '100%', border: 'none' }}
title={title ?? 'YouTube video'}
/>
</Box>
) : videoId != null ? (
<>
<Box
height={{
Expand Down
11 changes: 11 additions & 0 deletions libs/shared/ui/src/libs/deviceUtils/deviceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ export function isIOSTouchScreen(): boolean {
)
}

export function isInstagramAndroidWebView(): boolean {
if (
typeof navigator === 'undefined' ||
typeof navigator.userAgent === 'undefined'
)
return false

const ua = navigator.userAgent
return /Android/i.test(ua) && /Instagram/i.test(ua)
}

// TODO: should only resort to user agent sniffing as a last resport
export function isMobile(): boolean {
if (
Expand Down
1 change: 1 addition & 0 deletions libs/shared/ui/src/libs/deviceUtils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
hasTouchScreen,
isInstagramAndroidWebView,
isIPhone,
isMobile,
isIOS,
Expand Down
Loading