src/features/home/widgets/ and are documented in Live Activity and widgets.
Files
Endpoint
getToday(day) calls GET /v1/trainee/home. When a nutrition day hint is passed, it adds ?planId=...&dayId=....
MacroRing has a key (calories, protein, carbs, fat, water or steps), a label, current, goal and unit.
getToday normalises the response so every screen can trust the shape:
ringsdefaults to[].workout.subtitledefaults to''andworkout.estimatedMinutesto 0. A file plan sendsnullfor both.nextMeal.itemsdefaults to[].targetis kept only when present.mealsTodaystaysnullonly for an explicitnull(meals are not tracked for a file nutrition plan). A missing key becomes{ completed: 0, total: 0 }.weeklyWorkouts.totalmay benullfor a file plan without a weekly target.bannergoes throughnormalizeBanner(see below).filePlangoes throughnormalizeFilePlanFlags.
useHome
useHome() reads homeDayHintOf(state) from the nutrition session store and uses the query key [...homeQueryKey, planId, dayId], with a 30 second staleTime and placeholderData: keepPreviousData.
The hint is how the next meal follows the day the trainee picked in the nutrition tab. homeDayHintOf returns today’s pick when its dateKey matches today, otherwise an active preferred day, otherwise null. With no hint the server decides the day. Changing the picked day changes the query key, and keepPreviousData keeps the old cards on screen until the new response arrives.
Whenever data arrives, useHome calls syncHomeWidgets({ home: data }).
Other features invalidate the ['home'] prefix after a write, for example the weight and water mutations in Tracking and uploads.
Picking a skin
HomeScreen reads useHomeTheme() from src/features/branding/brandStore.ts and renders the matching screen. The value is brand.homeTheme, which the coach sets for the whole studio in the web branding settings. safeHomeTheme maps any unknown value to classic. See Theming and branding for how brand data reaches the store.
Every skin shows the same data and goes to the same places. Only the look changes, and the tab bar is the same in all of them. Other screens can follow the skin too: FormScreen derives its background from it.
Shared model
The three themed skins calluseHomeModel(), which mirrors the wiring the classic screen does inline:
All three themed skins refetch both the home query and the forms list on pull to refresh.
Classic
ClassicHomeScreen stacks shared components over an AmbientGlow that follows the scroll position:
HomeHero: greeting, the trainee and coach avatars (tap opens/profile), and a calendar button when enabled.SubscriptionPausedBanner(from the auth feature).NotificationsOffBanner.PendingFormsCard(from the forms feature).HomeBannerCard, when a banner exists.CaloriesPanel, orFilePlanNoticewhenfilePlan.nutritionis true.MovementStackwithsurface="home", whenhasMovementCards(data.movement, 'home')is true.ResumeWorkoutCardwhen a session is active, otherwiseNextWorkoutCardwhendata.workoutexists.- A floating
WhatsAppFabwhencoachWhatsappis set.
Bento
BentoHomeScreen uses a flat background and rounded borderless tiles, with Varela Round as the display face. Varela Round has one weight and no Arabic glyphs, so Arabic falls back to the app font tokens and bold moments use Heebo 700. Macros are drawn as dials (MacroDial). The banner is a small tile, BentoVideoCard. This skin does not render SkinBackground.
Glass
GlassHomeScreen renders SkinBackground skin="glass" and draws every surface as a frosted tile (Glass, radius 24) in Rubik, which covers Arabic. The background image is brand.storyBackgroundUrl only when the coach set one. Otherwise a brand-colour gradient carries the look. Macro bars are drawn locally (GlassBar). This skin adds two tiles the others do not have: a profile tile and a weight tile, which reads useMeasurementsSummary() and opens /measurements.
Poster
PosterHomeScreen renders SkinBackground skin="poster" with Karantina display type over Assistant body copy. Both fall back to the app fonts in Arabic. Pending forms are drawn as a solid brand “ticket”. The studio logo (brand.logoUrl) or studio name sits in the header. Progress uses SegmentBar, one block per unit. A goal above MAX_SEGMENTS = 12 collapses to 12 blocks filled in proportion.
Each themed skin calls
useBannerCta, a hook, from its own small banner component (BentoVideoCard, GlassBanner, PosterBannerTile) so the hook only runs when a banner exists. Keep that structure when editing a skin.Next workout card
TodaysWorkoutSummary carries id, title, subtitle, exerciseCount, estimatedMinutes, completed and an optional kind. For a file plan kind is file and id is the program id, because there is no day to open.
Navigation rules, identical in the classic screen and in useHomeModel:
What the workout screen does with the
start param is covered in Workout screens.
NextWorkoutCard shows subtitle || title and a meta line. For a normal plan that is the exercise count plus the estimated minutes when above 0. For a file plan it is the file-plan caption plus a done marker when completed, and the play icon becomes a check.
ResumeWorkoutCard replaces it while a session is active. It shows the next exercise from nextExercise(session) and a progress value of completed sets over total sets from sessionStats(session).
Nutrition panel and next meal card
CaloriesPanel has two pressable parts, both opening /nutrition:
- Macros. It finds the
caloriesring and theprotein,carbsandfatrings indata.rings, and draws one bar per macro withcurrent / goal. WhenmealsTodayis notnull, a ring showscompleted/totalmeals. - Next meal. Shown when
data.nextMealexists: the meal name,timeLabel, its energy, and itsitemsas a list.
useMbp().enabled), values are formatted with formatMbp and labelled with the studio’s portion label instead of grams and kcal.
Empty next meal
A coach can leave a meal empty for the trainee to compose, with only a limit. Itscalories is then 0 on the wire. useNextMealLimit(nextMeal) handles that case:
- It returns
nullwhen there is no next meal or the meal has items. - Otherwise it resolves
nextMeal.targetwithresolveMealTarget(target, mbpOn, anchors)and formats it withmealLimitDisplay.
value and unit in place of the energy, and the limit text in place of the item list.
Home banner
The home response carries at most one banner.mediaUrl is always a still frame. The card renders media through expo-image and cannot play it. For an uploaded video the backend sends a captured poster there and the playable URL in mediaVideoUrl.
normalizeBanner defaults mediaVideoUrl to null (older backends do not send it), turns a CTA without a kind into null, and fills CTA defaults: color: 'brand', icon: 'none'.
CTA behaviour
useBannerCta(banner, coachPhone) returns open, hidden, label, icon, tint, isVideo, thumbUrl, ctaPlaysBannerVideo and videoModal.
open() checks in this order:
kind: 'content'with acontentItemId: push/content/item/:id.kind: 'content'with no item but amediaVideoUrl: open the banner’s own video inContentVideoPlayer. An explicitly picked content item always wins over this.kind: 'whatsapp'with a coach phone: openwhatsappUrl(coachPhone).kind: 'link'with aurl:Linking.openURL(url).
cta.label, else the watch string when the CTA plays the banner video, else a per-kind fallback.
For mediaKind: 'youtube', thumbUrl is the hqdefault.jpg thumbnail from youtubeThumbnail(mediaUrl), because a YouTube page URL is not an image.
videoModal must be rendered by the caller. It is a native Modal, so it covers the whole screen from wherever it is mounted. The player is described in Content library.
Button look
ctaButtonTint(color, brand) returns a translucent white fill for glass, fixed solids with dark text for white, green and yellow, and the brand palette for brand. CTA_ICON maps each icon name to an Ionicons glyph. HomeBannerCard has two layouts: with a mediaUrl, the image under a dark gradient with a filled button, and without one, a text card whose CTA is a plain link in the brand colour, because the tints are designed for the dark gradient.
Pending forms
The classic screen usesPendingFormsCard. The themed skins map over model.pendingForms and call model.openForm(id). Both hide forms for a frozen plan and both refetch on focus. Details are in Forms rendering.
Notifications-off banner
NotificationsOffBanner shows while push permission is not granted and the user is signed in.
- With
canAskAgain, the button requests permission and then callsregisterPushToken(). - Otherwise the button opens the system settings with
Linking.openSettings(). - The close button hides it for
DISMISS_FOR_MS, 7 days. The dismissal time is stored throughpushBannerStorageunder the keyperform_push_banner_dismissed_at, and a module-scope flag keeps it hidden for the rest of the process. - Permission is re-read whenever the app becomes
active, so returning from settings clears the banner.
WhatsApp link
whatsappUrl(phone) strips non-digits and builds https://wa.me/.... A number that starts with +, 972 or 234 is used as is. Other numbers get a country prefix guessed from their shape: 972 in most cases, and 234 for an 11 digit number starting with 0 or a 10 digit number starting with 7, 8 or 9.
Components not currently mounted
CoachMessageCard, TodayCarousel, TodaysWorkoutCard and WeekSummary in src/features/home/components/ are not imported by any screen at the time of writing. HomeToday.lastMessage is still returned by the API and typed, but no mounted component shows it.
Profile screen
src/features/profile/presentation/ProfileScreen.tsx is the profile tab. Top to bottom:
LanguageRow
LanguageRow shows the current language name and opens an Alert with one button per entry in SUPPORTED_LANGUAGES (he, en, ar, de, es, fr). Picking one calls setLanguage(code). See RTL and language for what a switch does.
SubscriptionCard
SubscriptionCard reads user.subscription, user.planFrozen and user.appAccessWhileFrozen from the auth store. With no subscription, or status none, it shows a single line.
Otherwise it shows a status badge (active success, paused warning, scheduled and waiting info) and rows:
- The plan name row is always present.
- For
waitingwith awaitingForvalue, a row says what starts the subscription:intake_form,first_planorfirst_workout. - Start and end date rows show for every status except
waiting, where each shows only if the date exists. - A days-remaining row shows when
daysRemainingis not null. - When the plan is frozen,
appAccessWhileFrozenis true and the status ispaused, a note explains the trainee keeps app access. - When
subscription.nextexists, a second block shows the next subscription’s start and end dates.
Onboarding
src/features/onboarding/WelcomeScreen.tsx is the only file in the feature. src/app/index.tsx decides when it shows:
finishOnboarding(). A skip link calls it from any step.
finishOnboarding() in the auth store calls onboardingStorage.markFinished(), which stores 'true' under the key perform_onboarded, and sets hasFinishedOnboarding. The welcome screen stops showing once that key is set. See Authentication for what follows.