@fytkit/sdk 3.5.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to `@fytkit/sdk` are documented in this file.
4
4
  Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 4.1.0
8
+
9
+ To be published by hand with sdk-publish.yml (the manifest records 4.1.0). Create the tag and GitHub release `@fytkit/sdk@4.1.0` when it is published.
10
+
11
+ ### Features
12
+
13
+ * `subscribeToMessages` takes an optional `onCoachTyping`, for a "Sam is writing…" line: who on the trainee's coaching team is writing to them right now, as `{ kind: "typing", names }` (staff display names, as the conversation shows them) or `{ kind: "idle" }`. Heard only while the status is `live`; a name drops out by itself `COACH_TYPING_EXPIRY_MS` (8 seconds) after its last sign, so a lost stop never leaves it showing, and the state goes `idle` when the channel is lost. Additive. New type `CoachTypingState` and constant `COACH_TYPING_EXPIRY_MS` (W16)
14
+ * `programs.get` and `programs.getSession` carry `durationMinutes` on each session's workout: the trainer's planned time for the workout, null when unset. Additive (W13)
15
+ * `auth.getSecurityStatus()` adds `passwordChangedAt` (when the password was last set or changed, ISO 8601; null when the account has no password) and `backupCodesRemaining` (unused backup codes left; null when two-factor is off, 0 when every code is used), and `SecurityStatus` gains both fields. Additive (W20)
16
+ * `profile.get` and `profile.update` carry `lengthUnit` (`"in"` or `"cm"`), the member's unit for body measurements, beside `weightUnit`; `profile.update` takes it optionally. Additive (W19)
17
+ * `bodyMeasurements.record` takes an optional `clientRequestId` (a UUID) for offline replay, as `bodyMetrics.record` does: a replay with the same key returns the stored measurements instead of recording them twice. Calls without a key are unchanged (W21)
18
+
19
+ ### Bug Fixes
20
+
21
+ * `auth.disableTwoFactor` now signs out every other device, and the server rotates the caller's session; the SDK swaps its stored token transparently, so the caller stays signed in ([#494](https://github.com/cbarlow1993/fytkit-web/pull/494))
22
+
23
+ ## 4.0.0
24
+
25
+ To be published by hand (release-please, which runs on GitHub Actions, does not cut this release; the manifest records 4.0.0 so the next automated release bumps from it). Create the tag and GitHub release `@fytkit/sdk@4.0.0` when it is published, or release-please proposes 4.0.0 again.
26
+
27
+ ### ⚠ BREAKING CHANGES
28
+
29
+ * `appConfig.get` is removed, with no deprecation window: the `appConfig` namespace is gone from `FytkitClient`, and `GET /api/v1/app-config` and `POST /rpc/appConfig/get` answer 404. Use `app.bootstrap` instead. Its colours become the app theme's curated palette (`primaryColor` → `data.theme.palette.primary`, and so on), `appName` → `data.studio.name`, the links → `data.links`, `realtime` → `data.realtime`; the icon URLs, `buildStatus`, `buildVersion`, the social sign-in ids (`auth`), `id` and `trainerId` are gone. Field-by-field migration: [docs/developer/reference/app-bootstrap.md](https://github.com/cbarlow1993/fytkit-web/blob/main/docs/developer/reference/app-bootstrap.md#migrating-from-appconfigget) ([#535](https://github.com/cbarlow1993/fytkit-web/issues/535))
30
+ * `MessagesRealtimeSource` reads the Realtime settings from `app.bootstrap()` (`{ status: "ok", data: { realtime } }`) instead of `appConfig.get()`. A `FytkitClient` still satisfies it; a hand-written source must rename `appConfig.get` to `app.bootstrap` ([#535](https://github.com/cbarlow1993/fytkit-web/issues/535))
31
+
32
+ ### Features
33
+
34
+ * `app.bootstrap({ etag? })`: the app's launch read. The studio's display name and logo, the effective app theme (template, theme, palette, derived variants and the oldest app release that paints it), every template's minimum app version, feature overrides, the studio's minimum app version, the terms, privacy and help links, the Realtime settings and the payment policy, in one request that needs the client id alone. Resolves to `{ status: "ok", data, etag }`, or to `{ status: "unchanged", etag }` when the `etag` you stored is still current (the server answers 304 with no body). New types `AppBootstrapData`, `AppBootstrapOptions`, `AppBootstrapResult` and `AppNamespace`; `FytkitClient.clientId` names the client id the client was created with, to key per-studio caches by ([#535](https://github.com/cbarlow1993/fytkit-web/issues/535))
35
+ * `app.bootstrap`'s `data.paymentPolicy` and `resolvePaymentMode(policy, { platform, storefront, conditionsMet? })`: whether the app shows "Subscribe" (a link out to the studio's checkout) or "What your plan includes" for the storefront the device's store reports (StoreKit's alpha-3 `Storefront.countryCode` on iOS, Play Billing's alpha-2 country code on Android; case-insensitive). The policy is Fytkit's reviewed list of storefronts where linking out is allowed, with any conditions the app must meet first; every other storefront, and any unmet or unknown condition, is plan info. New types `PaymentMode`, `PaymentPlatform`, `PaymentPolicy` and `ResolvePaymentModeOptions` ([#538](https://github.com/cbarlow1993/fytkit-web/issues/538))
36
+ * `resolveJoinCode(code, { baseUrl?, fetch? })`: resolves a studio's join code (the code on its Fytkit Coach QR code or join link) to the studio's client id, name, logo URL and app theme, with no client id or session. An unknown or replaced code rejects with `{ kind: "not_found", refusal: { reason: "join_code_inactive" } }`; new types `JoinCodeResolution`, `JoinCodeTheme` and `ResolveJoinCodeOptions` ([#529](https://github.com/cbarlow1993/fytkit-web/issues/529))
37
+ * `resolveJoinCode`'s `studio` gains `coaches` (at most 3; today the studio's own public coach profile alone: `name` and the public profile image as `avatarUrl`, null when there is none; never staff names or emails) and `programCount` (published programmes, any access level; drafts and archived ones never count), for a welcome screen's "Coached by" row and programme count. Additive. New type `JoinCodeCoach` ([#600](https://github.com/cbarlow1993/fytkit-web/issues/600))
38
+ * `FytkitRefusalReason` gains `join_code_inactive` ([#529](https://github.com/cbarlow1993/fytkit-web/issues/529))
39
+ * `auth.resendVerification()`: mails a new email-verification link to the signed-in member's own address (a session is required; there is no email input), for a "Send it again" link on a confirm-your-email screen. Resolves to `{ status: "sent" }`, or to `{ status: "already_verified" }` with nothing sent when the address is verified already. Throttled per account (one send a minute, five an hour): over the limit it rejects with `{ kind: "rate_limited" }`. Fytkit Coach members get the app-link URL, as at sign-up. A bearer call, so a 401 refreshes or signs out as usual. Additive. New type `ResendVerificationResult` ([#615](https://github.com/cbarlow1993/fytkit-web/issues/615))
40
+ * `auth.confirmEmailChange({ token })`: applies an email change from the token in the confirmation link, for an app that opens that link itself (Fytkit Coach's app link). Needs no session; idempotent while the address still holds ([#532](https://github.com/cbarlow1993/fytkit-web/issues/532))
41
+ * `sessionLogs.create` and `sessionLogs.addSet` take an optional `clientRequestId` (a UUID) so an offline queue or a retry can replay a write safely: a replay with the same key returns the stored log or set instead of creating a duplicate log or failing the set with `conflict`, and a key reused for another log or set is `conflict`. On a replayed `create`, `sets` holds the log's current sets, so its type is now the set array instead of `never[]`. Calls without a key are unchanged ([#603](https://github.com/cbarlow1993/fytkit-web/issues/603))
42
+ * `bodyMetrics.record` and `nutritionLog.logFood` take an optional `clientRequestId` (a UUID) for the same offline replay: a replay with the same key returns the stored weigh-in (with the warning `update` would give it) or food log entry, with their current values, instead of recording a second one, and writes no second timeline event. A `logFood` key reused for another `loggedFor` day is `conflict`; a weigh-in's values can all be edited, so a `record` replay returns the stored weigh-in whatever it sends. Keys are scoped to the trainee. `nutritionLog.updateFood` and `nutritionLog.setTargets` take no key: they set absolute values, so a replay is already safe. Calls without a key are unchanged (W17, W18)
43
+ * `sessionLogs.delete({ id })`: deletes one of the member's own session logs, in progress or finished, with its sets, personal records and any staff review, comments and reactions; its timeline events go too, and the streak, personal records and program enrollment completion are recomputed. Resolves to `{ data: { id, deleted: true } }`; another member's log or an unknown id is `not_found`, and the call is refused while a member is impersonated ([#602](https://github.com/cbarlow1993/fytkit-web/issues/602))
44
+ * member coaching loop Phase 2: the `bodyMeasurements`, `progressPhotos`, `timeline`, `health` and `habits` namespaces, `progress.getVolume`, `progression.getE1rmTrend`, `bodyMetrics.update`, and the refusal reasons `staff_entered`, `checkin_sourced`, `consent_required`, `consent_outdated`, `too_many_pending`, `photo_limit_reached`, `invalid_uploads` and `poses_differ`, with the `coaching_habits` entitlement refusal and the `first_party_account` error code ([#461](https://github.com/cbarlow1993/fytkit-web/issues/461)) ([#487](https://github.com/cbarlow1993/fytkit-web/issues/487))
45
+
7
46
  ## [3.5.0](https://github.com/cbarlow1993/fytkit-web/compare/@fytkit/sdk@3.4.0...@fytkit/sdk@3.5.0) (2026-10-10)
8
47
 
9
48
 
package/README.md CHANGED
@@ -48,6 +48,42 @@ await fytkit.auth.signOut();
48
48
  off();
49
49
  ```
50
50
 
51
+ ## Joining a studio and launching
52
+
53
+ A Fytkit Coach style app starts from a studio's join code (its QR code or
54
+ join link) and reads the studio's launch settings once per launch:
55
+
56
+ ```ts
57
+ import { createFytkitClient, resolveJoinCode, resolvePaymentMode } from "@fytkit/sdk";
58
+
59
+ // Once, when the member scans or types the code: keep the client id.
60
+ const studio = await resolveJoinCode(scannedCode);
61
+ const fytkit = createFytkitClient({ clientId: studio.clientId });
62
+
63
+ // Every launch: revalidate the stored settings with their ETag.
64
+ const stored = await loadStoredBootstrap(); // { data, etag } | null, your storage
65
+ const result = await fytkit.app.bootstrap({ etag: stored?.etag });
66
+ if (result.status === "ok") await saveStoredBootstrap({ data: result.data, etag: result.etag });
67
+ const settings = result.status === "ok" ? result.data : stored?.data;
68
+ // settings.studio (name, logoUrl), settings.theme (palette, variants),
69
+ // settings.featureOverrides, settings.minAppVersion, settings.links,
70
+ // settings.realtime, settings.paymentPolicy
71
+
72
+ // "Subscribe" (a link out to the studio's checkout) only where the store allows it.
73
+ const { mode } = resolvePaymentMode(settings.paymentPolicy, {
74
+ platform: "ios",
75
+ storefront: storefrontCountryCode, // StoreKit's Storefront.current?.countryCode
76
+ });
77
+ ```
78
+
79
+ `app.bootstrap` resolves to `{ status: "unchanged" }` (a 304, no body) while
80
+ the `etag` you sent is current. It replaced `appConfig.get` in 4.0.0:
81
+ [`app.bootstrap`](../../docs/developer/reference/app-bootstrap.md), with the
82
+ field-by-field migration. [`resolveJoinCode`](../../docs/developer/reference/resolveJoinCode.md)
83
+ needs no client. `resolvePaymentMode` is plan info ("What your plan includes")
84
+ for every storefront the policy doesn't list:
85
+ [Subscribe or what your plan includes](../../docs/developer/reference/app-bootstrap.md#subscribe-or-what-your-plan-includes).
86
+
51
87
  ## Tracking a workout
52
88
 
53
89
  Log a workout ad-hoc (no program enrollment needed), record sets, complete
@@ -125,6 +161,13 @@ await fytkit.auth.changePassword({ currentPassword, newPassword, revokeOtherSess
125
161
  // nothing changes until the trainee opens it.
126
162
  await fytkit.auth.changeEmail({ newEmail });
127
163
 
164
+ // Resend the sign-up verification email to the trainee's own address (there
165
+ // is no email input). Resolves to { status: "sent" } or, when the address is
166
+ // already verified and nothing is sent, { status: "already_verified" }.
167
+ // Throttled per account (one a minute, five an hour): over the limit it
168
+ // rejects with { kind: "rate_limited", retryAfter } (milliseconds).
169
+ const { status } = await fytkit.auth.resendVerification();
170
+
128
171
  // Devices signed in through this app (plus the calling one); the trainee's
129
172
  // sessions elsewhere are never listed. revokeId is an opaque id (never the
130
173
  // bearer token).
@@ -143,7 +186,11 @@ fresh sign-in is `{ kind: "forbidden", code: "reauth_required" }`. None of
143
186
  these sign the trainee out — only a 401 on an authenticated call means the
144
187
  session is gone. A wrong password on `signIn` (for example, signing in again
145
188
  after `reauth_required`) is a plain `auth_required` and leaves the current
146
- session alone. Enrolling twice in a program is `{ kind: "conflict" }` with the
189
+ session alone. An account that also runs or works at a trainer on Fytkit can't
190
+ sign in, sign up or complete two-factor through an app, and an existing app
191
+ session can't change its password or email, resend its verification email or close it:
192
+ `{ kind: "forbidden", code: "first_party_account" }`; point it to Fytkit's own
193
+ sign-in. Enrolling twice in a program is `{ kind: "conflict" }` with the
147
194
  existing `enrollmentId`.
148
195
 
149
196
  ## Refusals
@@ -174,7 +221,7 @@ Every reason and its data: [`docs/developer/reference/errors.md`](../../docs/dev
174
221
 
175
222
  `subscribeToMessages` keeps a conversation screen current over Supabase
176
223
  Realtime: it fetches a token (`messages.realtimeToken`) and fytkit's public
177
- connection settings (`appConfig.get`), joins the trainee's own private
224
+ connection settings (`app.bootstrap`), joins the trainee's own private
178
225
  channel (and joins it afresh after Realtime closes it), renews the token
179
226
  before it expires, and reports when to poll instead. Events carry no message content: `onChange` means "read the
180
227
  conversation and the unread count again".
@@ -211,6 +258,23 @@ const unsubscribe = subscribeToMessages(fytkit, {
211
258
  unsubscribe();
212
259
  ```
213
260
 
261
+ To show "Sam is writing…" while someone on the trainee's coaching team
262
+ writes a reply, pass `onCoachTyping`. It reports `{ kind: "typing", names }`
263
+ (display names, as the conversation shows them on their messages) and
264
+ `{ kind: "idle" }`. It is live only: a name drops out by itself 8 seconds
265
+ after its last sign (`COACH_TYPING_EXPIRY_MS`), so a lost "stopped" never
266
+ leaves it showing, it goes `idle` when the channel is lost, and while you
267
+ poll it never shows.
268
+
269
+ ```ts
270
+ subscribeToMessages(fytkit, {
271
+ realtime,
272
+ onChange: () => refreshConversation(),
273
+ onCoachTyping: (typing) =>
274
+ setWritingLine(typing.kind === "typing" ? `${firstName(typing.names[0])} is writing…` : null),
275
+ });
276
+ ```
277
+
214
278
  Polling spends your app's per-minute request limit, which all its users
215
279
  share, so stop as soon as the status is `live`. The full guide:
216
280
  [`messages.realtimeToken`](../../docs/developer/reference/data-namespaces.md#messagesrealtimetoken)
@@ -227,13 +291,14 @@ await fytkit.preferences.updateNotifications({ streakAlerts: false }); // partia
227
291
 
228
292
  ## What's included in this preview
229
293
 
294
+ - `resolveJoinCode` (a studio's join code to its client id, name, logo and app theme, with no client) and `app.bootstrap` (the launch read, revalidated with its ETag), with `resolvePaymentMode` ("Subscribe" link-out or "What your plan includes", per storefront)
230
295
  - `auth.signUp` / `signIn` / `signOut` / `getSession` / `onAuthStateChange`
231
- - `auth.requestPasswordReset` / `confirmPasswordReset` / `confirmEmail`
296
+ - `auth.requestPasswordReset` / `confirmPasswordReset` / `confirmEmail` / `resendVerification`
232
297
  - Two-factor: `auth.verifyTotp` / `verifyBackupCode` / `requestEmailOtp` / `verifyEmailOtp` (sign-in), `enableTotp` / `verifyTotpSetup` / `disableTwoFactor` / `regenerateBackupCodes` / `getSecurityStatus` (management)
233
- - Account security: `auth.changePassword` / `changeEmail` / `listSessions` / `revokeSession` / `revokeOtherSessions` / `closeAccount`
234
- - Data namespaces: `workouts`, `programs`, `recipes`, `nutrition`, `nutritionLog`, `profile`, `preferences`, `articles`, `enrollments`, `sessionLogs`, `progression`, `progress`, `bodyMetrics`, `checkIns`, `messages`, `media`, `appConfig`, `subscription`
298
+ - Account security: `auth.changePassword` / `changeEmail` / `confirmEmailChange` / `listSessions` / `revokeSession` / `revokeOtherSessions` / `closeAccount`
299
+ - Data namespaces: `workouts`, `programs`, `recipes`, `nutrition`, `nutritionLog`, `profile`, `preferences`, `articles`, `enrollments`, `sessionLogs`, `progression`, `progress`, `bodyMetrics`, `bodyMeasurements`, `progressPhotos`, `checkIns`, `messages`, `timeline`, `health`, `habits`, `media`, `subscription`
235
300
  - Workout tracking: program session logs and ad-hoc workout logs (`sessionLogs.create` with `workoutId`), set logging, per-workout history (`sessionLogs.list({ workoutId })`)
236
- - Progress: `progress.getSummary` / `getAdherence` / `getStreak` (stored streaks), `progression.getPrSummary` / `listPrHistory` (stored PR history) / `getHistory`, `bodyMetrics.list` / `record` / `delete`
301
+ - Progress: `progress.getSummary` / `getAdherence` / `getStreak` (stored streaks) / `getVolume` (last week's hard sets per muscle group), `progression.getPrSummary` / `listPrHistory` (stored PR history) / `getE1rmTrend` (estimated 1-rep max per session, per lift) / `getHistory`, `bodyMetrics.list` / `record` / `update` / `delete`
237
302
  - Coach feedback on logged sessions: `sessionLogs.getFeedback` / `markFeedbackSeen` / `unseenFeedbackCount`
238
303
  - Check-ins: `checkIns.current` / `submit` / `update` / `list` / `get` / `markReplySeen` / `requestPhotoUpload` (weekly or fortnightly check-ins a coach reviews; needs a plan with `coaching_checkins`)
239
304
  - Messages: `messages.list` / `send` / `requestAttachmentUpload` / `markRead` / `delete` / `unreadCount` / `realtimeToken` (the trainee's conversation with their coaches; starting one needs a plan with `coaching_messages`, answering a coach does not; `requestAttachmentUpload` reserves a photo to send with a message)