@ fytkit/ sdk (英语)
Vanilla TypeScript SDK用于fytkit公共API. 在您的 fytkit 程序、 工作、 食谱和 Mux 保护的视频上, 建一个定制的见习软件 .
v0.1.x是一个前1.0放行. 每种导出类型的形状在
etc/sdk.api.md; 断开更改记录在CHANGELOG.md。 。 。 .
安装
pnpm add @fytkit/sdk @orpc/client
# or
npm install @fytkit/sdk @orpc/client
在SDK为前 1. 0 时按精确的版本 :
pnpm add @fytkit/[email protected] @orpc/client
完全安装 + 版本连接 : docs/developer/installation.md. semver政策: docs/developer/concepts/versioning.md。 。 。 .
30秒快速启动
import { createFytkitClient } from "@fytkit/sdk";
const fytkit = createFytkitClient({
clientId: "fk_app_xxxxxxxxxxxxxxxxx", // from /dashboard/developer
});
// Sign a trainee in (your own auth UI calls this)
const session = await fytkit.auth.signIn({
email: "[email protected]",
password: "their-password",
});
// Fetch their workouts
const { items } = await fytkit.workouts.list({ limit: 20 });
// Subscribe to auth state (e.g. to redirect on sign-out)
const off = fytkit.auth.onAuthStateChange((state) => {
if (state.type === "signed_out") window.location.assign("/sign-in");
});
// Sign out — revokes the server-side session and clears local storage
await fytkit.auth.signOut();
off();
加入一个工作室并启动
一个Fytkit教练风格的应用从工作室的联想代码(它的QR代码或 并阅读工作室的发射设置:
import { createFytkitClient, resolveJoinCode, resolvePaymentMode } from "@fytkit/sdk";
// Once, when the member scans or types the code: keep the client id.
const studio = await resolveJoinCode(scannedCode);
const fytkit = createFytkitClient({ clientId: studio.clientId });
// Every launch: revalidate the stored settings with their ETag.
const stored = await loadStoredBootstrap(); // { data, etag } | null, your storage
const result = await fytkit.app.bootstrap({ etag: stored?.etag });
if (result.status === "ok") await saveStoredBootstrap({ data: result.data, etag: result.etag });
const settings = result.status === "ok" ? result.data : stored?.data;
// settings.studio (name, logoUrl), settings.theme (palette, variants),
// settings.featureOverrides, settings.minAppVersion, settings.links,
// settings.realtime, settings.paymentPolicy
// "Subscribe" (a link out to the studio's checkout) only where the store allows it.
const { mode } = resolvePaymentMode(settings.paymentPolicy, {
platform: "ios",
storefront: storefrontCountryCode, // StoreKit's Storefront.current?.countryCode
});
app.bootstrap 决定: { status: "unchanged" } (a 304,无尸体)
页:1 etag 您发送是当前。 换了 appConfig.get 在4.0.0中选择:
app.bootstrap,与
逐地迁移. resolveJoinCode
不需要客户端. resolvePaymentMode 是计划信息 ("你的计划包括什么")
保险单上没有列出每个商店:
订阅或您的计划包括。 。 。 .
训练员预览
教练不能签入会员应用程序:员工账户在那里被拒
(单位:千美元)first_party_account)取而代之的是App →主题上的"app中的审查"
仪表板显示链接的QR代码,例如:
https://www.trainerlane.com/app-link/preview?client=<clientId>&token=<token>. 培训员Lane
教练赎回来的 没有成员的工作室只读预览
(未释放;下一个未成年人的船舶):
import { createFytkitClient, createMemoryStorageAdapter, refusalData } from "@fytkit/sdk";
// Start from the link's `client` and `token`. Use a dedicated client: the
// phone may be bound to no studio, or to another one, and a preview never
// shares a client with a signed-in member.
const url = new URL(scannedLink);
const preview = createFytkitClient({
clientId: url.searchParams.get("client") ?? "",
storage: createMemoryStorageAdapter(),
});
const session = await preview.auth.startPreview({ token: url.searchParams.get("token") ?? "" });
// session: { kind: "preview", accessToken, expiresAt }: show a Preview banner.
// Studio content works as for a member on any plan: app.bootstrap, programs,
// workouts, exercises, articles, recipes, nutrition, media, plans.
const programs = await preview.programs.list({});
// Anything a member owns (profile, progress, logs, check-ins, habits,
// messages, health) and every write is refused with `preview_mode`:
// show the screen's empty state.
try {
await preview.progress.getStreak();
} catch (err) {
if (!refusalData(err, "preview_mode")) throw err;
showEmptyState();
}
// End it (signOut does the same while previewing).
await preview.auth.endPreview();
- 一次,15分钟内. QR令牌在15分钟之内赎回一次
显示。 一个未知的,过期的,使用过的 或其他工作室的代号拒绝
preview_token_invalid(单位:千美元)not_found:向教练索取新代码. A 类 工作室自己的应用程序(不是Fytkit教练)preview_unavailable。 。 。 . - 60分钟,只有记忆. 预览会持续60分钟,永远不会
清新而永无止境
storage,所以重装结束。 结束于expiresAt打开endPreview或者说signOut,任何401(工作室转向) 当一个成员签到同一个客户时 每端 但最后的喷出signed_out· ;onPreviewChange报告每个开始和 结束. - 不与一员并.
startPreview本地拒绝member_session_active(单位:千美元)conflict),而一名成员则在其中签字 客户端.
反应 : usePreviewSession, (中文(简体) ), useStartPreview 财务报告和审定财务报表 useEndPreview 内
@fytkit/react (释出. 每一种方法:
auth.startPreview。 。 。 .
追踪健身
登录一个解决临时选择( 不需要程序注册), 记录集, 完成 然后读回进展:
// Start an ad-hoc log against any workout — or pass { sessionId, enrollmentId }
// instead to log a scheduled program session.
const { data: log } = await fytkit.sessionLogs.create({ workoutId });
await fytkit.sessionLogs.addSet({
id: log.id,
exerciseId,
setNumber: 1,
reps: 8,
weightKg: "80.00",
});
await fytkit.sessionLogs.update({ id: log.id, completedAt: new Date().toISOString() });
// Workout-by-workout history for that workout
const { data: history } = await fytkit.sessionLogs.list({ workoutId });
// Stored progress: streak, PR history, aggregate summary
const { data: streak } = await fytkit.progress.getStreak({});
const { data: prs } = await fytkit.progression.listPrHistory({ exerciseId });
const { data: summary } = await fytkit.progress.getSummary({ period: "12w" });
二因子登录
auth.signIn 返回歧视性结果。 当学员有2FA时
启用时,以他们选择的方法完成挑战:
const result = await fytkit.auth.signIn({ email, password });
if (result.kind === "two_factor_required") {
const { challenge } = result; // challenge.availableMethods: "totp" | "backup_code" | "email_otp"
// TOTP from their authenticator app…
const session = await fytkit.auth.verifyTotp({ challenge, code: "123456" });
// …or a one-time backup code:
// await fytkit.auth.verifyBackupCode({ challenge, code });
// …or an emailed code:
// await fytkit.auth.requestEmailOtp({ challenge });
// await fytkit.auth.verifyEmailOtp({ challenge, code });
}
每个验证方法缓存已发行的会话完全像普通的登录
(单位:千美元)signed_in 开火 onAuthStateChange) (中文(简体) ).
2FA 注册和账户安全
所有这一切都需要签署会议:
// Enroll: render totpUri as a QR code; show backupCodes exactly once.
const { totpUri, backupCodes } = await fytkit.auth.enableTotp({ password });
await fytkit.auth.verifyTotpSetup({ code: "123456" }); // SDK swaps the rotated token for you
const status = await fytkit.auth.getSecurityStatus(); // { twoFactorEnabled, totpEnabled, hasBackupCodes }
await fytkit.auth.regenerateBackupCodes({ password });
await fytkit.auth.disableTwoFactor({ password });
// Password change (verifies the current password server-side). With
// revokeOtherSessions every other session of the account ends, in any app,
// and the SDK adopts the rotated token transparently.
await fytkit.auth.changePassword({ currentPassword, newPassword, revokeOtherSessions: true });
// Email change — a confirmation link is emailed to the new address;
// nothing changes until the trainee opens it.
await fytkit.auth.changeEmail({ newEmail });
// Resend the sign-up verification email to the trainee's own address (there
// is no email input). Resolves to { status: "sent" } or, when the address is
// already verified and nothing is sent, { status: "already_verified" }.
// Throttled per account (one a minute, five an hour): over the limit it
// rejects with { kind: "rate_limited", retryAfter } (milliseconds).
const { status } = await fytkit.auth.resendVerification();
// Devices signed in through this app (plus the calling one); the trainee's
// sessions elsewhere are never listed. revokeId is an opaque id (never the
// bearer token).
const sessions = await fytkit.auth.listSessions();
await fytkit.auth.revokeSession({ revokeId: sessions.find((s) => !s.isCurrent)!.revokeId });
await fytkit.auth.revokeOtherSessions(); // "sign out of your other devices" on this app
// Permanently close the account (re-verifies email + password). On success the
// SDK clears the session and emits signed_out.
await fytkit.auth.closeAccount({ confirmEmail, password });
密码错误或一次性代码为 { kind: "server", status: 400 } 错误
与 code: "invalid_password" 编号 : "invalid_code"; 需要改变
新签注是 { kind: "forbidden", code: "reauth_required" }无
签下受训者——只有401个经认证的电话意味着
课会结束。 密码错误 signIn (例如再次签署)
之后 reauth_required是一个平地 auth_required 离开电流
单独开会。 在Fytkit的训练员上运行或工作的账户不能
通过应用程序和现有应用程序签名、注册或完成两个要素
会话无法更改密码或电子邮件, 无法重新发送其校验邮件或关闭它 :
{ kind: "forbidden", code: "first_party_account" }指给Fytkit自己看
注册。 两次在程序中注册是 { kind: "conflict" } 与
现有 enrollmentId。 。 。 .
拒绝
当服务器因某种原因拒绝时, 您的应用程序可以采取行动( 教练功能)
计划缺乏,一个已经发送的报到, 一个消息限制,一个工作室是
, 错误带有 refusal: { reason, data } 除了平常的田园.
分会 refusal.reason,从未在消息文本上; refusalData 读取
已知原因的数据已输入 :
import { refusalData } from "@fytkit/sdk";
try {
await fytkit.messages.send({ body, clientRequestId });
} catch (err) {
const plan = refusalData(err, "entitlement_required");
if (plan) return showUpgrade(plan.entitlement);
const daily = refusalData(err, "daily_limit");
if (daily) return keepDraftUntil(daily.retryAt);
throw err;
}
所有原因及其数据: docs/developer/reference/errors.md。 。 。 .
实时聊天
subscribeToMessages 在Supabase 上保持对讲屏
实时: 它获取一个符号( Q)messages.realtimeToken)和fytkit的公众
连接设置( e)app.bootstrap),加入受训者自己的私人
频道(并在实时关闭后重新加入), 刷新符号
并报告何时投票 没有消息内容的事件 : onChange 意思是读取
谈话和未读数".
SDK并不依赖Supabase -js. 添加 @supabase/supabase-js 改为
您的应用程序( y)npm install @supabase/supabase-js@^2.97.0并通过一个工厂
仅为订阅创建一个客户端。 必须是2.87.1或
后:助教用手将学员的道具放入插座上,并
更早的2.x客户端在下个心跳时将它换为肛门键
将订阅保留在 fallback保留 auth 以下选项:
客户端只携带学员的标志,不得保存或刷新
语句,或从页面的 URL 读取一个将替换它的签名 .
import { createClient } from "@supabase/supabase-js";
import { MESSAGES_FALLBACK_POLL_MS, subscribeToMessages } from "@fytkit/sdk";
const unsubscribe = subscribeToMessages(fytkit, {
realtime: ({ url, anonKey }) =>
createClient(url, anonKey, {
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
}),
onChange: () => refreshConversation(), // messages.list + messages.unreadCount
onStatus: (status) => {
// "live": stop polling. "connecting" / "fallback": poll the conversation
// every MESSAGES_FALLBACK_POLL_MS.conversation (10 s) and the count every
// MESSAGES_FALLBACK_POLL_MS.unreadCount (60 s). "refused": no trainee to
// listen for (signed out); the subscription has stopped.
setPolling(status === "connecting" || status === "fallback");
},
});
// On leaving the screen or signing out:
unsubscribe();
给大家看"Sam在写作" 而训练队的教练
写回信、通行证 onCoachTyping报告 { kind: "typing", names }
(播放姓名,正如谈话显示在他们的信息中)和
{ kind: "idle" }。它仅是活的:一个名字本身就消失了8秒
在其最后的标志之后(COACH_TYPING_EXPIRY_MS),所以一个迷失的"停止"从来没有
离开它显示,它去 idle 当频道丢失,而你
民意调查从不显示.
subscribeToMessages(fytkit, {
realtime,
onChange: () => refreshConversation(),
onCoachTyping: (typing) =>
setWritingLine(typing.kind === "typing" ? `${firstName(typing.names[0])} is writing…` : null),
});
Polling 花费您的应用程序的每分钟请求限制, 所有用户都会使用此限制
共享,所以当状态是 live。完整指南:
messages.realtimeToken
和食谱 与实时更新聊天屏幕。 。 。 .
通知偏好
成员切换被存储的服务器侧( 默认适用到第一次写入) :
const { data: prefs } = await fytkit.preferences.getNotifications();
await fytkit.preferences.updateNotifications({ streakAlerts: false }); // partial patch
coachMessages, (中文(简体) ), checkInReminders 财务报告和审定财务报表 coachFeedback 是推动的切换
(直到成员关机为止);“指定程序”按下
newPrograms。 。 。 .
推动通知
Fytkit教练为签约成员注册了设备的"博览会"推克.
代币绑在它注册的会话上 所以签了,
取消该会话或关闭账户会将其删除到服务器上; 呼叫
unregister 首先,所以电话停止 被推一次.
await fytkit.push.register({ token: expoPushToken, platform: "ios" }); // or "android"
await fytkit.push.unregister({ token: expoPushToken }); // idempotent
一个工作室自己的开发者应用被拒绝 { kind: "forbidden" }: 仅
Fytkit教练收到推力. 每一次推车 data 是一个 PushNotificationData
(单位:千美元){ v: 1, clientId, kind, href },没有消息文本); 忽略
clientId 工作室不是固定的 合同:
文件/开发者/指南/push-notections.md。 。 。 .
此预览中包含的内容
resolveJoinCode(一个工作室与其客户端id,名称,标志和app主题的联想代码,没有客户端)并app.bootstrap(发射读取,用其ETag重新验证)resolvePaymentMode("订阅"链接出或"你的计划包括什么",每个商店前端)auth.signUp编号 :signIn编号 :signOut编号 :getSession编号 :onAuthStateChangeauth.signInWithIdToken(与Apple或Google签约,Fytkit教练仅:通过生非ce;Apple获得其SHA-256六,Google获得生非ce)- 训练员预览 :
auth.startPreview编号 :getPreview编号 :onPreviewChange编号 :endPreview(只读,无成员预览从"app中的预览"QR代码;参见: 训练员预览(中文(简体) ) auth.requestPasswordReset编号 :confirmPasswordReset编号 :confirmEmail编号 :resendVerification- 双因素:
auth.verifyTotp编号 :verifyBackupCode编号 :requestEmailOtp编号 :verifyEmailOtp(签名),enableTotp编号 :verifyTotpSetup编号 :disableTwoFactor编号 :regenerateBackupCodes编号 :getSecurityStatus(管理层) - 账户安全 :
auth.changePassword编号 :changeEmail编号 :confirmEmailChange编号 :listSessions编号 :revokeSession编号 :revokeOtherSessions编号 :closeAccount - 数据命名空间 :
workouts, (中文(简体) ),programs, (中文(简体) ),recipes, (中文(简体) ),nutrition, (中文(简体) ),nutritionLog, (中文(简体) ),profile, (中文(简体) ),preferences, (中文(简体) ),push, (中文(简体) ),articles, (中文(简体) ),enrollments, (中文(简体) ),sessionLogs, (中文(简体) ),progression, (中文(简体) ),progress, (中文(简体) ),bodyMetrics, (中文(简体) ),bodyMeasurements, (中文(简体) ),progressPhotos, (中文(简体) ),checkIns, (中文(简体) ),messages, (中文(简体) ),timeline, (中文(简体) ),health, (中文(简体) ),habits, (中文(简体) ),media, (中文(简体) ),subscription - 工作跟踪:程序会话日志和临时工作日志(
sessionLogs.create与workoutId),设置记录,每个工作历史(sessionLogs.list({ workoutId })(中文(简体) ) - 进展:
progress.getSummary编号 :getAdherence编号 :getStreak(存储记录) /getVolume(上周硬套每肌肉组),progression.getPrSummary编号 :listPrHistory(存储PR历史) /getE1rmTrend(每届估计1-雷普最大,每次升起)/getHistory, (中文(简体) ),bodyMetrics.list编号 :record编号 :update编号 :delete - 教练对记录的会议的反馈:
sessionLogs.getFeedback编号 :markFeedbackSeen编号 :unseenFeedbackCount - 登录 :
checkIns.current编号 :submit编号 :update编号 :list编号 :get编号 :markReplySeen编号 :requestPhotoUpload(每周或每两周向教练报到;需要与coaching_checkins(中文(简体) ) - 信件数 :
messages.list编号 :send编号 :requestAttachmentUpload编号 :markRead编号 :delete编号 :unreadCount编号 :realtimeToken(受训者与导师的对话; 开始一个计划需要与coaching_messages回答教练不;requestAttachmentUpload保留随信件一起发送的照片) - 实时聊天 :
subscribeToMessages(加入学员的私人实时频道messages.realtimeToken并说何时投票 实时聊天(中文(简体) ) - 自动信头注射( E)
X-TrainerLane-Client-Id, (中文(简体) ),Authorization: Bearer …(中文(简体) ) - 主动刷新~60 s 在令牌过期前( 下线时用后退) + 被动 401 重试; 只有服务器拒绝刷新标志退出
- 可插值存储( E)
localStorage默认, SSR 的内存+ cookie)
储存权衡
访问令牌在 localStorage 默认时,名称空格 clientId. 运行在您来源上的任何脚本都可以读取这些脚本——发送严格的CSP,并将SDK像任何其他具有认证的依赖性一样对待. 符號是服务器会话,持续时间可长达7天;签署,关闭账户或撤销开发者app结束其服务器侧.
使用 createMemoryStorageAdapter() 用于SSR+测试.
下一个
@fytkit/react——钩子+<FytkitVideo>examples/sdk-react——可运行的样本应用程序docs/sdk/quickstart.md——全向导