@basictech/react 0.6.0 → 0.7.0-beta.1

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.mjs CHANGED
@@ -408,22 +408,33 @@ async function deleteRecord({ projectId, accountId, tableName, id, token }) {
408
408
  import { validateSchema as validateSchema3, compareSchemas } from "@basictech/schema";
409
409
 
410
410
  // package.json
411
- var version = "0.6.0-beta.0";
411
+ var version = "0.6.0";
412
412
 
413
413
  // src/AuthContext.tsx
414
414
  import { jsx, jsxs } from "react/jsx-runtime";
415
+ var LocalStorageAdapter = class {
416
+ async get(key) {
417
+ return localStorage.getItem(key);
418
+ }
419
+ async set(key, value) {
420
+ localStorage.setItem(key, value);
421
+ }
422
+ async remove(key) {
423
+ localStorage.removeItem(key);
424
+ }
425
+ };
415
426
  var BasicContext = createContext({
416
427
  unicorn: "\u{1F984}",
417
428
  isAuthReady: false,
418
429
  isSignedIn: false,
419
430
  user: null,
420
- signout: () => {
421
- },
422
- signin: () => {
423
- },
431
+ signout: () => Promise.resolve(),
432
+ signin: () => Promise.resolve(),
433
+ signinWithCode: () => new Promise(() => {
434
+ }),
424
435
  getToken: () => new Promise(() => {
425
436
  }),
426
- getSignInLink: () => "",
437
+ getSignInLink: () => Promise.resolve(""),
427
438
  db: {},
428
439
  dbStatus: "LOADING" /* LOADING */
429
440
  });
@@ -540,7 +551,13 @@ run "npm install @basictech/react@${latestVersion}" to update`);
540
551
  };
541
552
  }
542
553
  }
543
- function BasicProvider({ children, project_id, schema, debug = false }) {
554
+ function BasicProvider({
555
+ children,
556
+ project_id,
557
+ schema,
558
+ debug = false,
559
+ storage
560
+ }) {
544
561
  const [isAuthReady, setIsAuthReady] = useState(false);
545
562
  const [isSignedIn, setIsSignedIn] = useState(false);
546
563
  const [token, setToken] = useState(null);
@@ -549,7 +566,56 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
549
566
  const [isReady, setIsReady] = useState(false);
550
567
  const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
551
568
  const [error, setError] = useState(null);
569
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
570
+ const [pendingRefresh, setPendingRefresh] = useState(false);
552
571
  const syncRef = useRef(null);
572
+ const storageAdapter = storage || new LocalStorageAdapter();
573
+ const STORAGE_KEYS = {
574
+ REFRESH_TOKEN: "basic_refresh_token",
575
+ USER_INFO: "basic_user_info",
576
+ AUTH_STATE: "basic_auth_state",
577
+ DEBUG: "basic_debug"
578
+ };
579
+ const isDevelopment = () => {
580
+ return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes("localhost") || window.location.hostname.includes("127.0.0.1") || window.location.hostname.includes(".local") || process.env.NODE_ENV === "development" || debug === true;
581
+ };
582
+ const cleanOAuthParamsFromUrl = () => {
583
+ if (window.location.search.includes("code") || window.location.search.includes("state")) {
584
+ const url = new URL(window.location.href);
585
+ url.searchParams.delete("code");
586
+ url.searchParams.delete("state");
587
+ window.history.pushState({}, document.title, url.pathname + url.search);
588
+ log("Cleaned OAuth parameters from URL");
589
+ }
590
+ };
591
+ useEffect(() => {
592
+ const handleOnline = () => {
593
+ log("Network came back online");
594
+ setIsOnline(true);
595
+ if (pendingRefresh) {
596
+ log("Retrying pending token refresh");
597
+ setPendingRefresh(false);
598
+ if (token) {
599
+ const refreshToken = token.refresh_token || localStorage.getItem("basic_refresh_token");
600
+ if (refreshToken) {
601
+ fetchToken(refreshToken).catch((error2) => {
602
+ log("Retry refresh failed:", error2);
603
+ });
604
+ }
605
+ }
606
+ }
607
+ };
608
+ const handleOffline = () => {
609
+ log("Network went offline");
610
+ setIsOnline(false);
611
+ };
612
+ window.addEventListener("online", handleOnline);
613
+ window.addEventListener("offline", handleOffline);
614
+ return () => {
615
+ window.removeEventListener("online", handleOnline);
616
+ window.removeEventListener("offline", handleOffline);
617
+ };
618
+ }, [pendingRefresh, token]);
553
619
  useEffect(() => {
554
620
  function initDb(options) {
555
621
  if (!syncRef.current) {
@@ -615,32 +681,68 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
615
681
  connectToDb();
616
682
  }
617
683
  }, [isSignedIn, shouldConnect]);
684
+ const connectToDb = async () => {
685
+ const tok = await getToken();
686
+ if (!tok) {
687
+ log("no token found");
688
+ return;
689
+ }
690
+ log("connecting to db...");
691
+ syncRef.current.connect({ access_token: tok }).catch((e) => {
692
+ log("error connecting to db", e);
693
+ });
694
+ };
618
695
  useEffect(() => {
619
- localStorage.setItem("basic_debug", debug ? "true" : "false");
620
- try {
621
- if (window.location.search.includes("code")) {
622
- let code = window.location?.search?.split("code=")[1].split("&")[0];
623
- const state = localStorage.getItem("basic_auth_state");
624
- if (!state || state !== window.location.search.split("state=")[1].split("&")[0]) {
625
- log("error: auth state does not match");
626
- setIsAuthReady(true);
627
- localStorage.removeItem("basic_auth_state");
628
- window.history.pushState({}, document.title, "/");
629
- return;
630
- }
631
- localStorage.removeItem("basic_auth_state");
632
- fetchToken(code);
633
- } else {
634
- let cookie_token = getCookie("basic_token");
635
- if (cookie_token !== "") {
636
- setToken(JSON.parse(cookie_token));
696
+ const initializeAuth = async () => {
697
+ await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? "true" : "false");
698
+ try {
699
+ if (window.location.search.includes("code")) {
700
+ let code = window.location?.search?.split("code=")[1].split("&")[0];
701
+ const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
702
+ if (!state || state !== window.location.search.split("state=")[1].split("&")[0]) {
703
+ log("error: auth state does not match");
704
+ setIsAuthReady(true);
705
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
706
+ cleanOAuthParamsFromUrl();
707
+ return;
708
+ }
709
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
710
+ cleanOAuthParamsFromUrl();
711
+ fetchToken(code);
637
712
  } else {
638
- setIsAuthReady(true);
713
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
714
+ if (refreshToken) {
715
+ log("Found refresh token in storage, attempting to refresh access token");
716
+ fetchToken(refreshToken);
717
+ } else {
718
+ let cookie_token = getCookie("basic_token");
719
+ if (cookie_token !== "") {
720
+ const tokenData = JSON.parse(cookie_token);
721
+ setToken(tokenData);
722
+ if (tokenData.refresh_token) {
723
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token);
724
+ }
725
+ } else {
726
+ const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO);
727
+ if (cachedUserInfo) {
728
+ try {
729
+ const userData = JSON.parse(cachedUserInfo);
730
+ setUser(userData);
731
+ setIsSignedIn(true);
732
+ log("Loaded cached user info for offline mode");
733
+ } catch (error2) {
734
+ log("Error parsing cached user info:", error2);
735
+ }
736
+ }
737
+ setIsAuthReady(true);
738
+ }
739
+ }
639
740
  }
741
+ } catch (e) {
742
+ log("error getting token", e);
640
743
  }
641
- } catch (e) {
642
- log("error getting cookie", e);
643
- }
744
+ };
745
+ initializeAuth();
644
746
  }, []);
645
747
  useEffect(() => {
646
748
  async function fetchUser(acc_token) {
@@ -655,10 +757,13 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
655
757
  log("error fetching user", user2.error);
656
758
  return;
657
759
  } else {
658
- document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
659
- if (window.location.search.includes("code")) {
660
- window.history.pushState({}, document.title, "/");
760
+ if (token?.refresh_token) {
761
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
661
762
  }
763
+ await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user2));
764
+ log("Cached user info in storage");
765
+ document.cookie = `basic_access_token=${token.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
766
+ document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
662
767
  setUser(user2);
663
768
  setIsSignedIn(true);
664
769
  setIsAuthReady(true);
@@ -674,8 +779,18 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
674
779
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
675
780
  if (isExpired) {
676
781
  log("token is expired - refreshing ...");
677
- const newToken = await fetchToken(token?.refresh);
678
- fetchUser(newToken.access_token);
782
+ try {
783
+ const newToken = await fetchToken(token?.refresh_token);
784
+ fetchUser(newToken.access_token);
785
+ } catch (error2) {
786
+ log("Failed to refresh token in checkToken:", error2);
787
+ if (error2.message.includes("offline") || error2.message.includes("Network")) {
788
+ log("Network issue - continuing with expired token until online");
789
+ fetchUser(token.access_token);
790
+ } else {
791
+ setIsAuthReady(true);
792
+ }
793
+ }
679
794
  } else {
680
795
  fetchUser(token.access_token);
681
796
  }
@@ -684,41 +799,97 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
684
799
  checkToken();
685
800
  }
686
801
  }, [token]);
687
- const connectToDb = async () => {
688
- const tok = await getToken();
689
- if (!tok) {
690
- log("no token found");
691
- return;
802
+ const getSignInLink = async (redirectUri) => {
803
+ try {
804
+ log("getting sign in link...");
805
+ if (!project_id) {
806
+ throw new Error("Project ID is required to generate sign-in link");
807
+ }
808
+ const randomState = Math.random().toString(36).substring(6);
809
+ await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState);
810
+ const redirectUrl = redirectUri || window.location.href;
811
+ if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
812
+ throw new Error("Invalid redirect URI provided");
813
+ }
814
+ let baseUrl2 = "https://api.basic.tech/auth/authorize";
815
+ baseUrl2 += `?client_id=${project_id}`;
816
+ baseUrl2 += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
817
+ baseUrl2 += `&response_type=code`;
818
+ baseUrl2 += `&scope=profile`;
819
+ baseUrl2 += `&state=${randomState}`;
820
+ log("Generated sign-in link successfully");
821
+ return baseUrl2;
822
+ } catch (error2) {
823
+ log("Error generating sign-in link:", error2);
824
+ throw error2;
692
825
  }
693
- log("connecting to db...");
694
- syncRef.current.connect({ access_token: tok }).catch((e) => {
695
- log("error connecting to db", e);
696
- });
697
826
  };
698
- const getSignInLink = () => {
699
- log("getting sign in link...");
700
- const randomState = Math.random().toString(36).substring(6);
701
- localStorage.setItem("basic_auth_state", randomState);
702
- let baseUrl2 = "https://api.basic.tech/auth/authorize";
703
- baseUrl2 += `?client_id=${project_id}`;
704
- baseUrl2 += `&redirect_uri=${encodeURIComponent(window.location.href)}`;
705
- baseUrl2 += `&response_type=code`;
706
- baseUrl2 += `&scope=profile`;
707
- baseUrl2 += `&state=${randomState}`;
708
- return baseUrl2;
827
+ const signin = async () => {
828
+ try {
829
+ log("signing in...");
830
+ if (!project_id) {
831
+ log("Error: project_id is required for sign-in");
832
+ throw new Error("Project ID is required for authentication");
833
+ }
834
+ const signInLink = await getSignInLink();
835
+ log("Generated sign-in link:", signInLink);
836
+ if (!signInLink || !signInLink.startsWith("https://")) {
837
+ log("Error: Invalid sign-in link generated");
838
+ throw new Error("Failed to generate valid sign-in URL");
839
+ }
840
+ window.location.href = signInLink;
841
+ } catch (error2) {
842
+ log("Error during sign-in:", error2);
843
+ if (isDevelopment()) {
844
+ setError({
845
+ code: "signin_error",
846
+ title: "Sign-in Failed",
847
+ message: error2.message || "An error occurred during sign-in. Please try again."
848
+ });
849
+ }
850
+ throw error2;
851
+ }
709
852
  };
710
- const signin = () => {
711
- log("signing in: ", getSignInLink());
712
- const signInLink = getSignInLink();
713
- window.location.href = signInLink;
853
+ const signinWithCode = async (code, state) => {
854
+ try {
855
+ log("signinWithCode called with code:", code);
856
+ if (!code || typeof code !== "string") {
857
+ return { success: false, error: "Invalid authorization code" };
858
+ }
859
+ if (state) {
860
+ const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
861
+ if (storedState && storedState !== state) {
862
+ log("State parameter mismatch:", { provided: state, stored: storedState });
863
+ return { success: false, error: "State parameter mismatch" };
864
+ }
865
+ }
866
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
867
+ cleanOAuthParamsFromUrl();
868
+ const token2 = await fetchToken(code);
869
+ if (token2) {
870
+ log("signinWithCode successful");
871
+ return { success: true };
872
+ } else {
873
+ return { success: false, error: "Failed to exchange code for token" };
874
+ }
875
+ } catch (error2) {
876
+ log("signinWithCode error:", error2);
877
+ return {
878
+ success: false,
879
+ error: error2.message || "Authentication failed"
880
+ };
881
+ }
714
882
  };
715
- const signout = () => {
883
+ const signout = async () => {
716
884
  log("signing out!");
717
885
  setUser({});
718
886
  setIsSignedIn(false);
719
887
  setToken(null);
720
888
  document.cookie = `basic_token=; Secure; SameSite=Strict`;
721
- localStorage.removeItem("basic_auth_state");
889
+ document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
890
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
891
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
892
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
722
893
  if (syncRef.current) {
723
894
  (async () => {
724
895
  try {
@@ -735,6 +906,27 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
735
906
  const getToken = async () => {
736
907
  log("getting token...");
737
908
  if (!token) {
909
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
910
+ if (refreshToken) {
911
+ log("No token in memory, attempting to refresh from storage");
912
+ try {
913
+ const newToken = await fetchToken(refreshToken);
914
+ if (newToken?.access_token) {
915
+ return newToken.access_token;
916
+ }
917
+ } catch (error2) {
918
+ log("Failed to refresh token from storage:", error2);
919
+ if (error2.message.includes("offline") || error2.message.includes("Network")) {
920
+ log("Network issue - continuing with potentially expired token");
921
+ const lastToken = localStorage.getItem("basic_access_token");
922
+ if (lastToken) {
923
+ return lastToken;
924
+ }
925
+ throw new Error("Network offline - authentication will be retried when online");
926
+ }
927
+ throw new Error("Authentication expired. Please sign in again.");
928
+ }
929
+ }
738
930
  log("no token found");
739
931
  throw new Error("no token found");
740
932
  }
@@ -742,8 +934,22 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
742
934
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
743
935
  if (isExpired) {
744
936
  log("token is expired - refreshing ...");
745
- const newToken = await fetchToken(token?.refresh);
746
- return newToken?.access_token || "";
937
+ const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
938
+ if (refreshToken) {
939
+ try {
940
+ const newToken = await fetchToken(refreshToken);
941
+ return newToken?.access_token || "";
942
+ } catch (error2) {
943
+ log("Failed to refresh expired token:", error2);
944
+ if (error2.message.includes("offline") || error2.message.includes("Network")) {
945
+ log("Network issue - using expired token until network is restored");
946
+ return token.access_token;
947
+ }
948
+ throw new Error("Authentication expired. Please sign in again.");
949
+ }
950
+ } else {
951
+ throw new Error("no refresh token available");
952
+ }
747
953
  }
748
954
  return token?.access_token || "";
749
955
  };
@@ -762,20 +968,66 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
762
968
  return cookieValue;
763
969
  }
764
970
  const fetchToken = async (code) => {
765
- const token2 = await fetch("https://api.basic.tech/auth/token", {
766
- method: "POST",
767
- headers: {
768
- "Content-Type": "application/json"
769
- },
770
- body: JSON.stringify({ code })
771
- }).then((response) => response.json()).catch((error2) => log("Error:", error2));
772
- if (token2.error) {
773
- log("error fetching token", token2.error);
774
- return;
775
- } else {
776
- setToken(token2);
971
+ try {
972
+ if (!isOnline) {
973
+ log("Network is offline, marking refresh as pending");
974
+ setPendingRefresh(true);
975
+ throw new Error("Network offline - refresh will be retried when online");
976
+ }
977
+ const token2 = await fetch("https://api.basic.tech/auth/token", {
978
+ method: "POST",
979
+ headers: {
980
+ "Content-Type": "application/json"
981
+ },
982
+ body: JSON.stringify({ code })
983
+ }).then((response) => response.json()).catch((error2) => {
984
+ log("Network error fetching token:", error2);
985
+ if (!isOnline) {
986
+ setPendingRefresh(true);
987
+ throw new Error("Network offline - refresh will be retried when online");
988
+ }
989
+ throw new Error("Network error during token refresh");
990
+ });
991
+ if (token2.error) {
992
+ log("error fetching token", token2.error);
993
+ if (token2.error.includes("network") || token2.error.includes("timeout")) {
994
+ setPendingRefresh(true);
995
+ throw new Error("Network issue - refresh will be retried when online");
996
+ }
997
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
998
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
999
+ document.cookie = `basic_token=; Secure; SameSite=Strict`;
1000
+ document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
1001
+ setUser({});
1002
+ setIsSignedIn(false);
1003
+ setToken(null);
1004
+ setIsAuthReady(true);
1005
+ throw new Error(`Token refresh failed: ${token2.error}`);
1006
+ } else {
1007
+ setToken(token2);
1008
+ setPendingRefresh(false);
1009
+ if (token2.refresh_token) {
1010
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
1011
+ log("Updated refresh token in storage");
1012
+ }
1013
+ document.cookie = `basic_access_token=${token2.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
1014
+ log("Updated access token in cookie");
1015
+ }
1016
+ return token2;
1017
+ } catch (error2) {
1018
+ log("Token refresh error:", error2);
1019
+ if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
1020
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1021
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1022
+ document.cookie = `basic_token=; Secure; SameSite=Strict`;
1023
+ document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
1024
+ setUser({});
1025
+ setIsSignedIn(false);
1026
+ setToken(null);
1027
+ setIsAuthReady(true);
1028
+ }
1029
+ throw error2;
777
1030
  }
778
- return token2;
779
1031
  };
780
1032
  const db_ = (tableName) => {
781
1033
  const checkSignIn = () => {
@@ -818,12 +1070,13 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
818
1070
  user,
819
1071
  signout,
820
1072
  signin,
1073
+ signinWithCode,
821
1074
  getToken,
822
1075
  getSignInLink,
823
1076
  db: syncRef.current ? syncRef.current : noDb,
824
1077
  dbStatus
825
1078
  }, children: [
826
- error && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
1079
+ error && isDevelopment() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
827
1080
  isReady && children
828
1081
  ] });
829
1082
  }