Smartwatch Companion
4league ships a companion app for Wear OS and watchOS. On a PRO account, referees, Team Managers, and organizers can start matches, log goals, cards, fouls, substitutions and personal mistakes directly from the wrist — every action is queued on the watch and retried until the phone confirms it. Free-plan users, and anyone on a match they can't edit, still get a live read-only view: score, clock and events. Fans, meanwhile, get a ranking-position widget on every plan, showing where their followed teams sit across every competition they play in.
The watch app is a companion to the 4league phone app — all authentication, API calls, and data writes flow through the phone. If the phone is out of Bluetooth range or the 4league app is force-quit, actions are queued on the watch and sent the moment the connection returns (see Offline Outbox) rather than failing outright.
Overview
The smartwatch companion has two main capabilities:
- Live match recording (PRO) — available to users with match-editing rights (organizers, referees, Team Managers) whose phone account is on an active PRO plan. Start a match, add goals / cards / fouls / substitutions / mistakes, review and edit past events, and end the match — all from the watch, with the final score, goals, cards and assists derived automatically. See the PRO requirement below for what a Free-plan user sees instead.
- Ranking-position widget — available to every user on every plan. Glanceable list of every competition your followed teams play in, showing current rank, points, and a daily ▲/▼ arrow vs. the previous day's snapshot.
The watch holds no session of its own and never calls the backend directly — every request is relayed through the paired phone, which makes the real, authenticated API call and replies over Bluetooth.
The PRO Requirement
Starting/pausing/ending a match and logging any event (goals, cards, fouls, substitutions, mistakes) from the watch requires the phone account that's paired to the watch to have an active PRO plan — not the team's plan, not the competition's plan. There is no way to check this from a Free account on the watch itself; every attempt is refused with the same reason.
What a Free-plan user with match-editing rights sees on the watch instead:
- The match list, live clock, current score and event list — fully visible, read-only.
- A lock notice on the live screen ("PRO — upgrade in the phone app") in place of the recording controls.
- No on-watch purchase flow of any kind — the upgrade has to happen in the 4league phone app. See Payments & Packages and Subscribe to PRO & Cancel.
Under the hood, the paired phone tells the watch whether its signed-in user is PRO every time it sends the match list, and the phone independently refuses every recording action from a non-PRO account even if an older watch build somehow still shows the controls — the phone, never the watch, is the enforcement point.
Platforms
| Platform | Tech stack | Target | Package / Bundle |
|---|---|---|---|
| Wear OS | Kotlin, Jetpack Compose for Wear, Material3 for Wear, Wearable Data Layer API | Wear OS 3+ (API 30+) | com.nativeuplab.fourleague |
| watchOS | Swift, SwiftUI, WatchConnectivity | watchOS 9+ | com.nativeuplab.fourleague.watchkitapp |
Both platforms now ship the full live-match recording flow. The two remaining, deliberate differences are the live-screen layout (Wear OS uses a swipeable pager for Actions/Mistakes; watchOS reaches the same screens through a menu instead of a swipe) and the ranking widget, which is Wear OS only. See the feature-parity table below.
Feature parity
Both watch apps share the same backend, the same PRO gate, and the same paired-phone-as-source-of-truth pattern.
| Feature | Wear OS | watchOS |
|---|---|---|
| Match list | ✓ | ✓ |
| Live match (score / timer / pause+resume) | ✓ | ✓ |
| Start match / End match with derived final score | ✓ | ✓ |
| Goal entry (scorer + assist) | ✓ | ✓ |
| Card / foul / substitution entry | ✓ | ✓ |
| Mistakes counter | ✓ — 3rd page of the live pager | ✓ — Actions → Mistakes |
| Event list (review / edit / delete) | ✓ | ✓ — Actions → Events |
| Offline outbox | ✓ | ✓ |
| Swipeable live-screen pager (score → actions → mistakes) | ✓ | ⚠ Same screens, reached via a menu instead of a swipe |
| Rankings widget | ✓ | ✗ |
Pairing & Session Sync
Installing the 4league watch app from the Play Store (Wear OS) or App Store (watchOS) is enough — there is no separate login on the watch. The phone app detects a paired watch with the companion installed and sends match data and the account's PRO status over the Wearable Data Layer / WatchConnectivity.
Install the watch app
From the phone's companion store, or directly on the watch if the user browses for "4league" on a Wear OS device.
Open the 4league phone app
Log in on the phone as usual. As soon as the phone detects a reachable watch, it can answer the watch's requests for matches and rankings.
Open the watch app
The waiting screen resolves as soon as the phone answers. The main menu then offers Matches and Rankings.
Live Match Screens
When the signed-in phone user has match-editing rights (organizer, referee, or Team Manager) and is on an active PRO plan, the watch exposes the full live-match recording flow. Anyone else sees the same score/clock/events read-only, per the PRO requirement above.
Wear OS navigation
Wear OS ├── WaitingScreen — pre-session ├── MatchListScreen — upcoming/in-progress matches │ └── LiveMatchScreen — a 3-page swipeable pager once the match is live │ ├── Page 1: score / clock — tap the score to open GoalEntryScreen │ ├── Page 2: LiveActionsPage — Card / Foul / Substitution / Events │ └── Page 3: MistakesPage — the mistakes counter └── RankingsScreen — ranking-position widget (all users) └── RankingDetailScreen — Top 5 of the tapped competition
watchOS navigation
watchOS ├── WaitingView — pre-session ├── MatchListView — upcoming/in-progress matches │ └── LiveMatchView — score, clock, Start/End │ └── ActionMenuView — Card / Foul / Substitution / Mistakes / Events │ ├── CardEntryView / FoulView / SubstitutionView / GoalEntryView │ ├── MistakesView — the mistakes counter │ └── EventListView — review / edit / delete └── (no Rankings screens on watchOS)
watchOS reaches every one of these through ActionMenuView's list rather than Wear OS's swipeable pager — same screens, different navigation shape. watchOS also doesn't yet render the phone/error context Wear OS shows on its waiting screen (which phone is answering, whether it's signed in).
The match list contains only matches the signed-in user has editing rights on. The phone app is the source of truth for that filtering — it computes the list against the live user session and pushes the resulting payload to the watch, so the watch itself does no permission checks and does not query the backend for match data.
Role gating
The watch app contains no role-check logic — it is a thin client. The phone (and backend) decide what to push: fans and followers see only the Rankings widget (Wear OS only — watchOS has no rankings screen); only users with match-editing rights for a given match (organizer, referee, or Team Manager) receive that match in the watch's match list, and only when that user is also PRO can they record anything against it (see the PRO requirement). The rankings endpoint additionally filters out competitions where visibility === 0 or !onboardingDone server-side (see backend/controllers/Competition.js), so private or unpublished competitions never reach the wrist.
Mistakes Counter
A personal-mistake tally sits alongside goals, cards and fouls — on Wear OS it's the third page of the live-match pager (swipe left twice from the score page); on watchOS it's reached from the action menu.
- Two columns, home team on the left and away team on the right.
- Each team's header shows that team's running total — every mistake charged to the team as a whole, plus every mistake charged to one of its players, counts toward it.
- Tapping a player's row adds one mistake to that specific player.
- Tapping the team header lets you charge a mistake to the whole team, or pick a player from the lineup instead.
Each team has its own Reset button, behind a confirmation step, that clears only that side's mistakes for this match — it never touches the other team's tally. Reset deletes those mistake records on the phone and the server too, not just on the watch, so there's no undo beyond re-tapping them back in manually. The button is disabled while that side already has zero mistakes.
Mistakes are the one event type with no dedicated edit screen — to correct a mis-tap, delete the entry from the event list and add it again, or use the per-side Reset for a clean slate.
Offline Outbox
Every action taken on the watch — not just ones taken while visibly disconnected — is queued locally first and only cleared once the phone confirms it landed. This is what lets a referee start a match, pocket the phone, and run the whole game from the wrist without worrying about a dropped Bluetooth link.
- A goal tapped out of range is stamped with the watch's own clock at the moment it was tapped, so when the connection returns it still lands at the correct match minute — not the minute the phone happened to receive it.
- The queue replays oldest first and stops at the first refusal, so capture order is always preserved; an item sent moments ago is skipped in case its confirmation is still in flight.
- A queued action already visible in the server's own event list (matched by team, player, kind and a close timestamp) is retired automatically — a lost confirmation can never produce a duplicate goal on retry.
- After several rejections in a row, an entry stops auto-retrying and is left for the referee to resolve manually.
- The live screen shows a pending count whenever anything is still queued, and a queued event shows in the event list at its correct minute, flagged as not yet confirmed.
Ending a Match Saves the Final Score
Tapping End on the watch now finishes the match exactly the way ending it from the phone does: the backend derives the final score, every player's goals and cards, and every goal's assist directly from the events recorded during the live match, and saves them the same way a phone-ended match is saved.
A match ended from the watch used to be able to close with no score recorded at all, since the watch has no way to build the phone's full save payload itself. The backend now derives that payload from the match's own recorded live events instead, so a watch-ended match is no longer a special, score-less case.
Reviewing, Editing & Deleting Events
Past events for the live match can be reviewed from the watch — Wear OS from the LiveActionsPage, watchOS from Actions → Events — newest first, each showing its minute, kind and the player involved.
- Edit re-opens the matching entry screen (goal, card, foul, substitution) pre-filled with that event's values; saving updates the existing event in place instead of creating a new one.
- Delete removes the event. Deleting a goal also removes its paired assist — the two are never left half-deleted.
Ranking-Position Widget
The rankings widget is a glanceable tile for every competition that a followed team plays in — visible to every plan (Free, PRO Player, PRO, Enterprise). It's designed so a fan who follows three teams across five competitions can see all five positions in one scroll without opening the phone.
What the user sees
Each tile on the Rankings screen shows:
- The organization name (if the competition belongs to one) as a small header.
- The team name — large, bold.
- The current rank (e.g. "#4") and points in that competition.
- A delta arrow on the right side:
- ▲ green — team moved up since the previous day's snapshot.
- ▼ red — team moved down.
- — grey — rank unchanged since yesterday.
- · dim — no prior snapshot yet (first-run or newly published competition). Rendered as a no-arrow state on purpose; zero and null are different states.
The backend emits delta = priorRank − currentRank. Positive means the team climbed positions (e.g. 5 → 3 gives delta = +2 → green ▲). This matches the sign convention the rankingPositionChange push notification uses, so the arrow the user saw in the notification matches the arrow on the watch tile.
Stale-data hint
If the last successful fetch is more than 5 minutes old while the Rankings screen is open, a small yellow "Updated >5m ago" line appears above the list. The screen re-evaluates staleness every 30 seconds while visible — the ticker only runs after the first response, so it doesn't burn battery before the user has any data.
Empty state
If the user follows no teams, or every followed team's competition is private/unpublished, the screen shows "No rankings available" plus a manual Refresh button. Private competitions are never shown on the watch to users who can't see them.
Backend endpoint
The Rankings screen is backed by a single endpoint — GET /competition/watch-rankings (also accepts POST for clients that prefer to send the team list in a body). It takes the followed team IDs as a repeated teamIds query param or a body array, walks each followed team's competitions, drops anything private (visibility === 0) or with onboardingDone === false, and returns the tile payload:
{
"tiles": [
{
"rankingId": "...",
"orgId": "...",
"orgName": "...",
"orgLogo": "...",
"teamId": "...",
"teamName": "...",
"teamCrest": "...",
"rank": 4,
"points": 27,
"delta": 2,
"top5": [ ... ]
}
]
}
The same pipeline that Competition.getStandings uses computes rank and points, so a tile's number always matches the standings screen on the phone. The top5 array is what powers RankingDetailScreen's Top 5 view.
Rankings Screens
RankingsScreen — the tile list
Vertical ScalingLazyColumn of RankingTileCards, one per (team, competition) pair. The screen requests fresh data on navigation and on manual refresh; the list renders whatever the repository last observed and updates reactively when fresh data arrives.
RankingDetailScreen — Top 5 of one competition
Tapping a tile opens a detail view that renders the top 5 rows of that competition's standings — the same ranking the user sees on the phone's standings screen. The followed team is highlighted with a green left-edge rule and bold text.
If the followed team is not in the top 5 (say they sit 9th), the screen shows the top 5, a ··· separator, and then one highlighted row for the followed team's current position.
Alex follows two teams — his son's U14 club and his local Sunday-league team. Between matches he raises his wrist, sees the U14 side has moved up one place (▲ +1) and the Sunday team held steady (—). He taps the U14 tile and confirms the club is now 2nd with the leader just 1 point ahead. Alex is on the Free plan, which is no obstacle here — the rankings widget is available on every plan.
Related: the rankingPositionChange push
Complementing the glanceable widget is an on-demand push notification (rankingPositionChange) that fires when a followed team's position moves relative to the organization-level ranking system. Tapping the notification deep-links into the phone app's RankingTable screen for the relevant organization.
Because the notification's positionChange uses the same positive-means-moved-up convention as the watch tile's delta, the ▲/▼ the user saw in the push matches the ▲/▼ they see when they raise their wrist a moment later.
Notification thresholds (how many positions a team must move before a push is fired) are configurable per organization from the organization profile — see that page's admin settings for the audit-logged threshold control.