@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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createORPCClient, ORPCError } from '@orpc/client';
1
+ import { ORPCError, isORPCErrorJson, createORPCErrorFromJson, createORPCClient } from '@orpc/client';
2
2
  import { RPCLink } from '@orpc/client/fetch';
3
3
 
4
4
  // src/error.ts
@@ -20,7 +20,8 @@ function pickRefusal(data) {
20
20
  if (!isRecord(data)) return {};
21
21
  const { reason, ...rest } = data;
22
22
  if (typeof reason === "string" && reason.length > 0) return { refusal: { reason, data: rest } };
23
- if (data.quotaCode === MEMBER_CAP_QUOTA) return { refusal: { reason: MEMBER_CAP_QUOTA, data: {} } };
23
+ if (data.quotaCode === MEMBER_CAP_QUOTA)
24
+ return { refusal: { reason: MEMBER_CAP_QUOTA, data: {} } };
24
25
  return {};
25
26
  }
26
27
 
@@ -95,6 +96,28 @@ function normalizeError(err) {
95
96
  return { kind: "server", status: 0 };
96
97
  }
97
98
 
99
+ // src/app.ts
100
+ var UNEXPECTED_NOT_MODIFIED = { kind: "server", status: 304 };
101
+ function createAppNamespace(transport) {
102
+ const bootstrap = async (options = {}) => {
103
+ const etag = options.etag ?? null;
104
+ let output;
105
+ try {
106
+ output = await transport.client.app.bootstrap(void 0, {
107
+ context: etag === null ? {} : { ifNoneMatch: etag }
108
+ });
109
+ } catch (error) {
110
+ throw normalizeError(error);
111
+ }
112
+ if (output === void 0 || output === null) {
113
+ if (etag === null) throw UNEXPECTED_NOT_MODIFIED;
114
+ return { status: "unchanged", etag };
115
+ }
116
+ return { status: "ok", data: output.data, etag: output.meta.etag };
117
+ };
118
+ return { bootstrap };
119
+ }
120
+
98
121
  // src/events.ts
99
122
  function createEventBus() {
100
123
  const listeners = /* @__PURE__ */ new Set();
@@ -235,6 +258,10 @@ function createAuthNamespace(opts) {
235
258
  const confirmEmail = async (input) => {
236
259
  await call(() => opts.transport.client.auth.confirmEmail(input));
237
260
  };
261
+ const resendVerification = async () => {
262
+ const result = await call(() => opts.transport.client.auth.resendVerification({}));
263
+ return result.data;
264
+ };
238
265
  const verifyTotp = async (input) => {
239
266
  const result = await call(
240
267
  () => opts.transport.client.auth.verifyTotp({ token: input.challenge.token, code: input.code })
@@ -289,7 +316,8 @@ function createAuthNamespace(opts) {
289
316
  adoptRotatedToken(result.data.accessToken);
290
317
  };
291
318
  const disableTwoFactor = async (input) => {
292
- await call(() => opts.transport.client.auth.disableTwoFactor(input));
319
+ const result = await call(() => opts.transport.client.auth.disableTwoFactor(input));
320
+ adoptRotatedToken(result.data.accessToken);
293
321
  };
294
322
  const regenerateBackupCodes = async (input) => {
295
323
  const result = await call(() => opts.transport.client.auth.regenerateBackupCodes(input));
@@ -308,6 +336,9 @@ function createAuthNamespace(opts) {
308
336
  const changeEmail = async (input) => {
309
337
  await call(() => opts.transport.client.auth.changeEmail(input));
310
338
  };
339
+ const confirmEmailChange = async (input) => {
340
+ await call(() => opts.transport.client.auth.confirmEmailChange(input));
341
+ };
311
342
  const listSessions = async () => {
312
343
  const result = await call(() => opts.transport.client.auth.listSessions());
313
344
  return result.data;
@@ -364,6 +395,7 @@ function createAuthNamespace(opts) {
364
395
  requestPasswordReset,
365
396
  confirmPasswordReset,
366
397
  confirmEmail,
398
+ resendVerification,
367
399
  verifyTotp,
368
400
  verifyBackupCode,
369
401
  requestEmailOtp,
@@ -375,6 +407,7 @@ function createAuthNamespace(opts) {
375
407
  regenerateBackupCodes,
376
408
  changePassword,
377
409
  changeEmail,
410
+ confirmEmailChange,
378
411
  listSessions,
379
412
  revokeSession,
380
413
  revokeOtherSessions,
@@ -384,6 +417,9 @@ function createAuthNamespace(opts) {
384
417
  };
385
418
  }
386
419
 
420
+ // src/base-url.ts
421
+ var DEFAULT_BASE_URL = "https://api.fytkit.com";
422
+
387
423
  // src/data.ts
388
424
  function wrapSlice(slice) {
389
425
  const cache = /* @__PURE__ */ new Map();
@@ -422,11 +458,15 @@ function createDataNamespaces(transport) {
422
458
  sessionLogs: wrapSlice(c.sessionLog),
423
459
  progression: wrapSlice(c.progression),
424
460
  bodyMetrics: wrapSlice(c.bodyMetric),
461
+ bodyMeasurements: wrapSlice(c.bodyMeasurement),
462
+ progressPhotos: wrapSlice(c.progressPhoto),
425
463
  checkIns: wrapSlice(c.checkin),
426
464
  messages: wrapSlice(c.message),
465
+ timeline: wrapSlice(c.timeline),
466
+ health: wrapSlice(c.health),
467
+ habits: wrapSlice(c.habit),
427
468
  progress: wrapSlice(c.progress),
428
469
  media: wrapSlice(c.media),
429
- appConfig: wrapSlice(c.appConfig),
430
470
  subscription: wrapSlice(c.subscription),
431
471
  profile: wrapSlice(c.profile),
432
472
  nutritionLog: wrapSlice(c.nutritionLog),
@@ -548,7 +588,7 @@ function createTransport(options) {
548
588
  };
549
589
  const link = new RPCLink({
550
590
  url: () => `${options.baseUrl}/rpc`,
551
- headers: () => {
591
+ headers: ({ context }) => {
552
592
  const session = options.getSession();
553
593
  const headers = {
554
594
  "X-Fytkit-Client-Id": options.clientId
@@ -556,6 +596,9 @@ function createTransport(options) {
556
596
  if (session) {
557
597
  headers.Authorization = `Bearer ${session.accessToken}`;
558
598
  }
599
+ if (context.ifNoneMatch !== void 0) {
600
+ headers["If-None-Match"] = context.ifNoneMatch;
601
+ }
559
602
  return headers;
560
603
  },
561
604
  fetch: wrappedFetch
@@ -575,6 +618,7 @@ var CREDENTIAL_PROCEDURES = [
575
618
  "auth/requestPasswordReset",
576
619
  "auth/confirmPasswordReset",
577
620
  "auth/confirmEmail",
621
+ "auth/confirmEmailChange",
578
622
  "auth/verifyTotp",
579
623
  "auth/verifyBackupCode",
580
624
  "auth/requestEmailOtp",
@@ -642,13 +686,82 @@ function attachRefreshInterceptor(opts) {
642
686
  return retried;
643
687
  });
644
688
  }
689
+ var INACTIVE = {
690
+ kind: "not_found",
691
+ refusal: { reason: "join_code_inactive", data: {} }
692
+ };
693
+ var IGNORED_SEPARATORS = /[\s\-\u2010-\u2015\u2212]/g;
694
+ var PATH_SAFE_CODE = /^[A-Za-z0-9]{1,64}$/;
695
+ function pathSafeJoinCode(input) {
696
+ const candidate = input.replace(IGNORED_SEPARATORS, "");
697
+ return PATH_SAFE_CODE.test(candidate) ? candidate.toUpperCase() : null;
698
+ }
699
+ async function readJson(response) {
700
+ try {
701
+ return await response.json();
702
+ } catch {
703
+ return void 0;
704
+ }
705
+ }
706
+ function refusalOf(response, body) {
707
+ const wait = () => retryAfterHeaderMs(response.headers.get("retry-after"), Date.now()) ?? 0;
708
+ if (isORPCErrorJson(body)) {
709
+ const error = normalizeError(createORPCErrorFromJson(body));
710
+ if (error.kind === "rate_limited" && error.retryAfter === 0) {
711
+ return { ...error, retryAfter: wait() };
712
+ }
713
+ return error;
714
+ }
715
+ if (response.status === 429) return { kind: "rate_limited", retryAfter: wait() };
716
+ return { kind: "server", status: response.status };
717
+ }
718
+ async function resolveJoinCode(code, options = {}) {
719
+ const canonical = pathSafeJoinCode(code);
720
+ if (canonical === null) throw INACTIVE;
721
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
722
+ const doFetch = options.fetch ?? fetch.bind(globalThis);
723
+ let response;
724
+ try {
725
+ response = await doFetch(`${baseUrl}/api/v1/join/${canonical}`, {
726
+ method: "GET",
727
+ headers: { Accept: "application/json" }
728
+ });
729
+ } catch (error) {
730
+ throw normalizeError(error);
731
+ }
732
+ const body = await readJson(response);
733
+ if (response.ok) {
734
+ if (isRecord(body) && isRecord(body.data)) {
735
+ return body.data;
736
+ }
737
+ throw { kind: "server", status: response.status };
738
+ }
739
+ throw refusalOf(response, body);
740
+ }
741
+
742
+ // src/payment-policy.ts
743
+ var PLAN_INFO = { mode: "plan_info" };
744
+ function resolvePaymentMode(policy, options) {
745
+ if (!policy || !Array.isArray(policy.linkOut)) return PLAN_INFO;
746
+ const storefront = options.storefront?.trim().toUpperCase() ?? "";
747
+ if (storefront === "") return PLAN_INFO;
748
+ const rule = policy.linkOut.find(
749
+ (entry) => entry?.platform === options.platform && typeof entry.storefront === "string" && entry.storefront.toUpperCase() === storefront
750
+ );
751
+ if (!rule || !Array.isArray(rule.conditions)) return PLAN_INFO;
752
+ const met = new Set(options.conditionsMet ?? []);
753
+ if (!rule.conditions.every((condition) => met.has(condition))) return PLAN_INFO;
754
+ return { mode: "link_out", conditions: rule.conditions };
755
+ }
645
756
 
646
757
  // src/realtime.ts
758
+ var COACH_TYPING_EXPIRY_MS = 8e3;
647
759
  var MESSAGES_FALLBACK_POLL_MS = {
648
760
  conversation: 1e4,
649
761
  unreadCount: 6e4
650
762
  };
651
763
  var THREAD_CHANGED_EVENT = "thread_changed";
764
+ var COACH_TYPING_EVENT = "coach_typing";
652
765
  var MEMBER_TOPIC_PREFIX = "member:";
653
766
  var TOKEN_RENEW_BEFORE_MS = 6e4;
654
767
  var TOKEN_MIN_DELAY_MS = 5e3;
@@ -672,6 +785,17 @@ function isFinalError(error) {
672
785
  function isMemberTopic(topic) {
673
786
  return typeof topic === "string" && topic.startsWith(MEMBER_TOPIC_PREFIX) && topic.length > MEMBER_TOPIC_PREFIX.length;
674
787
  }
788
+ function coachTypingSign(message) {
789
+ if (typeof message !== "object" || message === null || !("payload" in message)) return null;
790
+ const payload = message.payload;
791
+ if (typeof payload !== "object" || payload === null) return null;
792
+ if (!("state" in payload) || !("name" in payload)) return null;
793
+ const { state, name } = payload;
794
+ if (state !== "typing" && state !== "stopped") return null;
795
+ if (typeof name !== "string" || name.length === 0) return null;
796
+ const key = "key" in payload && typeof payload.key === "string" ? payload.key : "";
797
+ return { state, name, key: key.length > 0 ? `key:${key}` : `name:${name}` };
798
+ }
675
799
  function isConnection(value) {
676
800
  if (typeof value !== "object" || value === null) return false;
677
801
  if (!("url" in value) || !("anonKey" in value)) return false;
@@ -708,9 +832,45 @@ function subscribeToMessages(client, options) {
708
832
  let rejoinTimer = null;
709
833
  let closures = 0;
710
834
  let stopAuthListener = null;
835
+ const writing = /* @__PURE__ */ new Map();
836
+ let reportedNames = [];
837
+ const reportTyping = () => {
838
+ if (closed) return;
839
+ const names = [...new Set([...writing.values()].map((writer) => writer.name))];
840
+ if (names.length === reportedNames.length && names.every((name, i) => name === reportedNames[i])) {
841
+ return;
842
+ }
843
+ reportedNames = names;
844
+ options.onCoachTyping?.(names.length === 0 ? { kind: "idle" } : { kind: "typing", names });
845
+ };
846
+ const clearTyping = () => {
847
+ if (writing.size === 0) return;
848
+ for (const writer of writing.values()) clearTimeout(writer.expiry);
849
+ writing.clear();
850
+ reportTyping();
851
+ };
852
+ const onTypingSign = (sign) => {
853
+ const known = writing.get(sign.key);
854
+ if (known !== void 0) clearTimeout(known.expiry);
855
+ if (sign.state === "stopped") {
856
+ if (known === void 0) return;
857
+ writing.delete(sign.key);
858
+ reportTyping();
859
+ return;
860
+ }
861
+ writing.set(sign.key, {
862
+ name: sign.name,
863
+ expiry: setTimeout(() => {
864
+ writing.delete(sign.key);
865
+ reportTyping();
866
+ }, COACH_TYPING_EXPIRY_MS)
867
+ });
868
+ reportTyping();
869
+ };
711
870
  const report = (status) => {
712
871
  if (closed || status === lastStatus) return;
713
872
  lastStatus = status;
873
+ if (status !== "live") clearTyping();
714
874
  options.onStatus?.(status);
715
875
  };
716
876
  const clearTimer = () => {
@@ -738,6 +898,7 @@ function subscribeToMessages(client, options) {
738
898
  channel = null;
739
899
  cancelRejoin();
740
900
  closures = 0;
901
+ clearTyping();
741
902
  if (open === null) return;
742
903
  if (joined !== null) Promise.resolve(open.removeChannel(joined)).then(ignore, ignore);
743
904
  open.realtime.disconnect();
@@ -754,8 +915,10 @@ function subscribeToMessages(client, options) {
754
915
  const finish = (status) => {
755
916
  if (closed) return;
756
917
  const changed = status !== lastStatus;
918
+ const wasTyping = writing.size > 0;
757
919
  lastStatus = status;
758
920
  close();
921
+ if (wasTyping) options.onCoachTyping?.({ kind: "idle" });
759
922
  if (changed) options.onStatus?.(status);
760
923
  };
761
924
  const join = (joinTopic, open) => {
@@ -765,6 +928,9 @@ function subscribeToMessages(client, options) {
765
928
  const current = () => !closed && channel === joined;
766
929
  joined.on("broadcast", { event: THREAD_CHANGED_EVENT }, () => {
767
930
  if (current()) options.onChange();
931
+ }).on("broadcast", { event: COACH_TYPING_EVENT }, (message) => {
932
+ const sign = coachTypingSign(message);
933
+ if (sign !== null && current()) onTypingSign(sign);
768
934
  }).subscribe((status) => {
769
935
  if (!current()) return;
770
936
  if (status === "SUBSCRIBED") {
@@ -822,7 +988,8 @@ function subscribeToMessages(client, options) {
822
988
  if (connection === null) {
823
989
  let realtime;
824
990
  try {
825
- realtime = (await client.appConfig.get()).data.realtime;
991
+ const bootstrap = await client.app.bootstrap();
992
+ realtime = bootstrap.status === "ok" ? bootstrap.data.realtime : void 0;
826
993
  } catch (error) {
827
994
  if (closed) return;
828
995
  if (isFinalError(error)) {
@@ -899,7 +1066,6 @@ function subscribeToMessages(client, options) {
899
1066
  }
900
1067
 
901
1068
  // src/index.ts
902
- var DEFAULT_BASE_URL = "https://api.fytkit.com";
903
1069
  function createFytkitClient(options) {
904
1070
  if (!options || typeof options.clientId !== "string" || options.clientId.length === 0) {
905
1071
  throw new Error("createFytkitClient: `clientId` is required");
@@ -918,6 +1084,7 @@ function createFytkitClient(options) {
918
1084
  attachRefreshInterceptor({ transport, auth: authInstance });
919
1085
  const data = createDataNamespaces(transport);
920
1086
  return {
1087
+ clientId: options.clientId,
921
1088
  auth: {
922
1089
  signUp: authInstance.signUp,
923
1090
  signIn: authInstance.signIn,
@@ -927,6 +1094,7 @@ function createFytkitClient(options) {
927
1094
  requestPasswordReset: authInstance.requestPasswordReset,
928
1095
  confirmPasswordReset: authInstance.confirmPasswordReset,
929
1096
  confirmEmail: authInstance.confirmEmail,
1097
+ resendVerification: authInstance.resendVerification,
930
1098
  verifyTotp: authInstance.verifyTotp,
931
1099
  verifyBackupCode: authInstance.verifyBackupCode,
932
1100
  requestEmailOtp: authInstance.requestEmailOtp,
@@ -938,15 +1106,17 @@ function createFytkitClient(options) {
938
1106
  regenerateBackupCodes: authInstance.regenerateBackupCodes,
939
1107
  changePassword: authInstance.changePassword,
940
1108
  changeEmail: authInstance.changeEmail,
1109
+ confirmEmailChange: authInstance.confirmEmailChange,
941
1110
  listSessions: authInstance.listSessions,
942
1111
  revokeSession: authInstance.revokeSession,
943
1112
  revokeOtherSessions: authInstance.revokeOtherSessions,
944
1113
  closeAccount: authInstance.closeAccount
945
1114
  },
946
- ...data
1115
+ ...data,
1116
+ app: createAppNamespace(transport)
947
1117
  };
948
1118
  }
949
1119
 
950
- export { MESSAGES_FALLBACK_POLL_MS, createFytkitClient, createLocalStorageAdapter, createMemoryStorageAdapter, refusalData, subscribeToMessages };
1120
+ export { COACH_TYPING_EXPIRY_MS, MESSAGES_FALLBACK_POLL_MS, createFytkitClient, createLocalStorageAdapter, createMemoryStorageAdapter, refusalData, resolveJoinCode, resolvePaymentMode, subscribeToMessages };
951
1121
  //# sourceMappingURL=index.js.map
952
1122
  //# sourceMappingURL=index.js.map