@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 +39 -0
- package/README.md +71 -6
- package/dist/index.d.ts +2576 -942
- package/dist/index.js +179 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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 (`
|
|
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`, `
|
|
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)
|