The home tab is the trainee’s daily summary. It has one endpoint and four interchangeable looks. This page also covers the profile tab and the onboarding welcome screen at the end. Home screen widgets live under 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=....
Each 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:
  • rings defaults to [].
  • workout.subtitle defaults to '' and workout.estimatedMinutes to 0. A file plan sends null for both.
  • nextMeal.items defaults to []. target is kept only when present.
  • mealsToday stays null only for an explicit null (meals are not tracked for a file nutrition plan). A missing key becomes { completed: 0, total: 0 }.
  • weeklyWorkouts.total may be null for a file plan without a weekly target.
  • banner goes through normalizeBanner (see below).
  • filePlan goes through normalizeFilePlanFlags.

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 call useHomeModel(), 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:
  1. HomeHero: greeting, the trainee and coach avatars (tap opens /profile), and a calendar button when enabled.
  2. SubscriptionPausedBanner (from the auth feature).
  3. NotificationsOffBanner.
  4. PendingFormsCard (from the forms feature).
  5. HomeBannerCard, when a banner exists.
  6. CaloriesPanel, or FilePlanNotice when filePlan.nutrition is true.
  7. MovementStack with surface="home", when hasMovementCards(data.movement, 'home') is true.
  8. ResumeWorkoutCard when a session is active, otherwise NextWorkoutCard when data.workout exists.
  9. A floating WhatsAppFab when coachWhatsapp is 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 calories ring and the protein, carbs and fat rings in data.rings, and draws one bar per macro with current / goal. When mealsToday is not null, a ring shows completed/total meals.
  • Next meal. Shown when data.nextMeal exists: the meal name, timeLabel, its energy, and its items as a list.
When the studio uses the portion system (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. Its calories is then 0 on the wire. useNextMealLimit(nextMeal) handles that case:
  • It returns null when there is no next meal or the meal has items.
  • Otherwise it resolves nextMeal.target with resolveMealTarget(target, mbpOn, anchors) and formats it with mealLimitDisplay.
When it returns a value, the cards show the limit’s 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:
  1. kind: 'content' with a contentItemId: push /content/item/:id.
  2. kind: 'content' with no item but a mediaVideoUrl: open the banner’s own video in ContentVideoPlayer. An explicitly picked content item always wins over this.
  3. kind: 'whatsapp' with a coach phone: open whatsappUrl(coachPhone).
  4. kind: 'link' with a url: Linking.openURL(url).
The button is hidden when there is no CTA, or the CTA cannot do anything: WhatsApp without a coach phone, content with neither an item nor a playable video, or a link without a URL. The label is the trimmed 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 uses PendingFormsCard. 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 calls registerPushToken().
  • 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 through pushBannerStorage under the key perform_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.
See Notifications. 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 waiting with a waitingFor value, a row says what starts the subscription: intake_form, first_plan or first_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 daysRemaining is not null.
  • When the plan is frozen, appAccessWhileFrozen is true and the status is paused, a note explains the trainee keeps app access.
  • When subscription.next exists, 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:
The screen is a three-step carousel (training, nutrition, progress), each with a full-bleed image, a title and a body. The primary button advances a step and, on the last step, calls 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.