react-use#useLocalStorage TypeScript Examples

The following examples show how to use react-use#useLocalStorage. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx    From easy-email with MIT License 6 votes vote down vote up
PresetColorsProvider: React.FC<{}> = (props) => {
  const [currentColors, setCurrentColors] = useLocalStorage<string[]>(
    CURRENT_COLORS_KEY,
    defaultPresetColor
  );

  const colorDivRef = useRef(document.createElement('div'));

  const addCurrentColor = useCallback(
    (newColor: string) => {
      colorDivRef.current.style.color = '';
      colorDivRef.current.style.color = newColor;
      if (colorDivRef.current.style.color) {
        const newColors = [...new Set([...currentColors!, newColor])]
          .filter(Boolean)
          .slice(0, 16);
        setCurrentColors(newColors);
      }
    },
    [currentColors, setCurrentColors]
  );

  const value = useMemo(() => {
    return {
      colors: currentColors!,
      addCurrentColor,
    };
  }, [addCurrentColor, currentColors]);

  return useMemo(() => {
    return (
      <PresetColorsContext.Provider value={value}>
        {props.children}
      </PresetColorsContext.Provider>
    );
  }, [props.children, value]);
}
Example #2
Source File: useI18n.ts    From back-home-safe with GNU General Public License v3.0 6 votes vote down vote up
[UseI18nProvider, useI18n] = constate(() => {
  const [language, setLanguage] = useLocalStorage(
    "language",
    languageType["ZH-HK"]
  );
  const { i18n } = useTranslation();

  useEffect(() => {
    i18n.changeLanguage(language);
  }, [i18n, language]);

  return {
    language: language || languageType["ZH-HK"],
    setLanguage,
  };
})
Example #3
Source File: useMigration.ts    From back-home-safe with GNU General Public License v3.0 6 votes vote down vote up
useMigration = () => {
  const [, , removePasswordHash] = useLocalStorage<string | null>(
    "password_hash",
    null
  );

  useEffect(() => {
    // Old version store password with SHA256, which can be brute-forced
    removePasswordHash();
  }, [removePasswordHash]);

  const { unlocked, setValue } = useData();

  // Old versions travel records has no unique id
  useEffect(() => {
    setValue((prev) => ({
      ...prev,
      travelRecords: prev.travelRecords.map((item) => {
        if (has("id", item)) return item;
        return { ...(item as TravelRecord), id: uuid() };
      }),
    }));
  }, [unlocked, setValue]);
}
Example #4
Source File: HomeView.tsx    From joplin-utils with MIT License 6 votes vote down vote up
HomeView: React.FC = () => {
  const [language] = useLocalStorage<LanguageEnum>('language', getLanguage())
  return (
    <Card>
      <ReactMarkdown className={css.home}>
        {language === LanguageEnum.En ? README : README_ZH_CN}
      </ReactMarkdown>
    </Card>
  )
}
Example #5
Source File: AutoSaveAndRestoreEmail.tsx    From easy-email with MIT License 5 votes vote down vote up
export function AutoSaveAndRestoreEmail() {
  const formState = useFormState<any>();
  const { reset, mutators } = useForm();
  const { id = 'new' } = useQuery<{ id: string }>();

  const [currentEmail, setCurrentEmail] =
    useLocalStorage<IEmailTemplate | null>(id, null);
  const dirty = getIsFormTouched(formState.touched as any);

  const [visible, setVisible] = useState(Boolean(currentEmail));

  useEffect(() => {
    if (dirty) {
      setCurrentEmail(formState.values);
    }
  }, [dirty, formState.values, setCurrentEmail]);

  useInterval(() => {
    if (dirty) {
      setCurrentEmail(formState.values);
    }
  }, 5000);

  const onRestore = () => {
    if (currentEmail) {
      reset(currentEmail);
      setCurrentEmail(null);
      setVisible(false);
      mutators.setFieldTouched(Object.keys(formState.touched || {})[0], true);
    }
  };

  const onDiscard = () => {
    setCurrentEmail(null);
    setVisible(false);
  };

  const onBeforeConfirm = () => {
    setCurrentEmail(null);
  };

  return (
    <>
      <Modal
        title='Restore email?'
        visible={Boolean(visible && currentEmail)}
        onOk={onRestore}
        okText='Restore'
        cancelText='Discard'
        onCancel={onDiscard}
        style={{ zIndex: 10000 }}
      >
        <p>Are you want to restore unsaved email?</p>
      </Modal>
      <WarnAboutUnsavedChanges onBeforeConfirm={onBeforeConfirm} />
    </>
  );
}
Example #6
Source File: context.tsx    From storefront with MIT License 5 votes vote down vote up
AuthProvider: React.FC = (props) => {
  const router = useRouter();

  const [userData, setUserData] = useLocalStorage<UserData>('authToken');

  /**
   * Login the user.
   *
   * @param newUserData
   * @param urlToRedirect URL where user needs to be redirected after login.
   */
  const login = (newUserData: UserData, urlToRedirect = '/my-account') => {
    // Set the authToken, user ID and username in localStorage.
    setUserData(newUserData);

    // Redirect the user to the given URL.
    if (urlToRedirect != null) {
      router.push(urlToRedirect);
    }
  };

  /**
   * Logout the user.
   *
   * @param urlToRedirect URL where user needs to be redirected after logout.
   */
  const logout = (urlToRedirect = '/login') => {
    // Set auth data value in localStorage to empty.
    setUserData(undefined);

    // Redirect the user to the given URL.
    if (urlToRedirect != null) {
      router.push(urlToRedirect);
    }
  };

  return (
    <AuthContext.Provider
      value={{
        authToken: userData?.authToken,
        login,
        logout,
        userData: userData?.authToken == null ? null : userData,
      }}
      {...props}
    />
  );
}
Example #7
Source File: useCamera.ts    From back-home-safe with GNU General Public License v3.0 5 votes vote down vote up
[UseCameraProvider, useCamera] = constate(() => {
  const [hasCameraSupport] = useState("mediaDevices" in navigator);

  const [preferredCameraId, setPreferredCameraId] = useLocalStorage(
    "preferred_camera_id",
    "AUTO"
  );
  const [cameraList, setCameraList] = useState<MediaDeviceInfo[] | null>(null);

  const initCameraList = useCallback(async () => {
    try {
      if (
        !hasCameraSupport ||
        !hasIn("enumerateDevices", navigator.mediaDevices)
      ) {
        setCameraList([]);
        return;
      }

      const deviceList = await navigator.mediaDevices.enumerateDevices();

      const cameraList = deviceList.filter<MediaDeviceInfo>(
        (device): device is MediaDeviceInfo => device.kind === "videoinput"
      );

      setCameraList(cameraList);
    } catch (e) {
      alert("Unable to list device.\n\n" + e);
    }
  }, [hasCameraSupport]);

  useEffect(() => {
    initCameraList();
  }, [hasCameraSupport, initCameraList]);

  useEffect(() => {
    if (
      cameraList !== null &&
      preferredCameraId !== "AUTO" &&
      !any(({ deviceId }) => deviceId === preferredCameraId, cameraList)
    ) {
      setPreferredCameraId("AUTO");
    }
  }, [cameraList, setPreferredCameraId, preferredCameraId]);

  return {
    preferredCameraId: !any(
      ({ deviceId }) => deviceId === preferredCameraId,
      cameraList || []
    )
      ? "AUTO"
      : preferredCameraId,
    cameraList: cameraList || [],
    setPreferredCameraId,
    hasCameraSupport,
  };
})
Example #8
Source File: useStakeSignatures.tsx    From mStable-apps with GNU Lesser General Public License v3.0 5 votes vote down vote up
useStakeSignatures = () =>
  useLocalStorage<StakeSignatures>('stakeSignatures', {}) as unknown as [StakeSignatures, Dispatch<SetStateAction<StakeSignatures>>]
Example #9
Source File: LayoutView.tsx    From joplin-utils with MIT License 5 votes vote down vote up
LayoutView: React.FC = () => {
  const [language, setLanguage] = useLocalStorage<LanguageEnum>(
    'language',
    getLanguage(),
  )
  const [{ value: list }, fetch] = useAsyncFn(
    async (language: LanguageEnum) => {
      console.log('language: ', language)
      await i18n.init({ en, zhCN }, language)
      return routeList.map((item) => ({
        ...item,
        title: i18n.t(item.title as any),
      }))
    },
    [],
  )

  useMount(() => fetch(language!))

  const [refreshKey, { inc }] = useCounter(0)
  async function changeLanguage(value: LanguageEnum) {
    setLanguage(value)
    await fetch(value)
    inc()
  }
  return (
    <Layout className={css.app}>
      <Layout.Sider className={css.sider} width="max-content">
        <h2 className={css.logo}>Joplin Batch</h2>
        <Menu>
          {list &&
            list.map((item) => (
              <Menu.Item key={item.path as string}>
                <Link to={item.path as string}>{item.title}</Link>
              </Menu.Item>
            ))}
        </Menu>
      </Layout.Sider>
      <Layout>
        <Layout.Header className={css.header}>
          <Select
            options={[
              { label: 'English', value: LanguageEnum.En },
              { label: '中文', value: LanguageEnum.ZhCN },
            ]}
            value={language}
            onChange={changeLanguage}
          />
        </Layout.Header>
        <Layout.Content className={css.main}>
          {list && <RouterView key={refreshKey} />}
        </Layout.Content>
      </Layout>
    </Layout>
  )
}
Example #10
Source File: SettingsView.tsx    From joplin-utils with MIT License 5 votes vote down vote up
SettingsView: React.FC = () => {
  const [form] = Form.useForm<Config>()

  async function onFinish() {
    if (!(await form.validateFields())) {
      return
    }
    const values = form.getFieldsValue()
    console.log('onFinish: ', values)
    try {
      joplinApiGenerator.token = values.token
      joplinApiGenerator.baseUrl = values.baseUrl
      await joplinApiGenerator.noteApi.list({ limit: 1 })
      setSettings(values)
      message.success(i18n.t('settings.msg.success'))
    } catch (e) {
      console.error(e)
      message.error(i18n.t('settings.msg.error'))
    }
  }

  const [settings, setSettings] = useLocalStorage<Config>('settings')

  return (
    <Card>
      <h2>{i18n.t('settings.title')}</h2>
      <Form
        form={form}
        onFinish={onFinish}
        initialValues={
          { token: settings?.token, baseUrl: settings?.baseUrl ?? 'http://localhost:41184' } as Partial<Config>
        }
      >
        <Form.Item
          name={'baseUrl' as keyof Config}
          label={i18n.t('settings.form.baseUrl')}
          rules={[{ required: true }]}
        >
          <Input type={'url'} />
        </Form.Item>
        <Form.Item name={'token' as keyof Config} label={i18n.t('settings.form.token')} rules={[{ required: true }]}>
          <Input.Password />
        </Form.Item>
        <Form.Item>
          <Button type={'primary'} htmlType={'submit'}>
            {i18n.t('settings.action.submit')}
          </Button>
        </Form.Item>
      </Form>
    </Card>
  )
}
Example #11
Source File: collection.tsx    From react-notion-x with MIT License 4 votes vote down vote up
CollectionViewBlock: React.FC<{
  block: types.CollectionViewBlock | types.CollectionViewPageBlock
  className?: string
}> = ({ block, className }) => {
  const { recordMap, showCollectionViewDropdown } = useNotionContext()
  const { view_ids: viewIds } = block
  const collectionId = getBlockCollectionId(block, recordMap)

  const [isMounted, setIsMounted] = React.useState(false)
  React.useEffect(() => {
    setIsMounted(true)
  }, [])

  const defaultCollectionViewId = viewIds[0]
  const [collectionState, setCollectionState] = useLocalStorage(block.id, {
    collectionViewId: defaultCollectionViewId
  })

  const collectionViewId =
    (isMounted &&
      viewIds.find((id) => id === collectionState.collectionViewId)) ||
    defaultCollectionViewId

  const onChangeView = React.useCallback(
    (collectionViewId: string) => {
      console.log('change collection view', collectionViewId)

      setCollectionState({
        ...collectionState,
        collectionViewId
      })
    },
    [collectionState, setCollectionState]
  )

  let { width: windowWidth } = useWindowSize()
  if (isServer) {
    windowWidth = 1024
  }

  const collection = recordMap.collection[collectionId]?.value
  const collectionView = recordMap.collection_view[collectionViewId]?.value
  const collectionData =
    recordMap.collection_query[collectionId]?.[collectionViewId]
  const parentPage = getBlockParentPage(block, recordMap)

  const { style, width, padding } = React.useMemo(() => {
    const style: React.CSSProperties = {}

    if (collectionView?.type !== 'table' && collectionView?.type !== 'board') {
      return {
        style,
        width: 0,
        padding: 0
      }
    }

    const width = windowWidth
    // TODO: customize for mobile?
    const maxNotionBodyWidth = 708
    let notionBodyWidth = maxNotionBodyWidth

    if (parentPage?.format?.page_full_width) {
      notionBodyWidth = (width - 2 * Math.min(96, width * 0.08)) | 0
    } else {
      notionBodyWidth =
        width < maxNotionBodyWidth
          ? (width - width * 0.02) | 0 // 2vw
          : maxNotionBodyWidth
    }

    const padding =
      isServer && !isMounted ? 96 : ((width - notionBodyWidth) / 2) | 0
    style.paddingLeft = padding
    style.paddingRight = padding

    return {
      style,
      width,
      padding
    }
  }, [windowWidth, parentPage, collectionView?.type, isMounted])

  // console.log({
  //   width,
  //   padding
  // })

  if (!(collection && collectionView && collectionData)) {
    console.warn('skipping missing collection view for block', block.id, {
      collectionId,
      collectionViewId,
      collectionView,
      collectionData,
      recordMap
    })
    return null
  }

  const title = getTextContent(collection.name).trim()
  if (collection.icon) {
    block.format = {
      ...block.format,
      page_icon: collection.icon
    }
  }

  return (
    <div className={cs('notion-collection', className)}>
      <div className='notion-collection-header' style={style}>
        {title && (
          <div className='notion-collection-header-title'>
            <PageIcon
              block={block}
              className='notion-page-title-icon'
              hideDefaultIcon
            />

            {title}
          </div>
        )}

        {viewIds.length > 1 && showCollectionViewDropdown && (
          <CollectionViewDropdownMenu
            collectionView={collectionView}
            collectionViewId={collectionViewId}
            viewIds={viewIds}
            onChangeView={onChangeView}
          />
        )}
      </div>

      <CollectionView
        collection={collection}
        collectionView={collectionView}
        collectionData={collectionData}
        padding={padding}
        width={width}
      />
    </div>
  )
}
Example #12
Source File: index.tsx    From easy-email with MIT License 4 votes vote down vote up
export function useCollection() {
  const [collection, setCollection] = useLocalStorage(
    COLLECTION_KEY,
    defaultData
  );

  const addCollection = useCallback(
    (payload: CollectedBlock) => {
      if (!collection) return;

      collection[0].blocks.push({
        id: payload.id,
        title: payload.label,
        description: payload.helpText,
        thumbnail: payload.thumbnail,
        data: payload.data,
      });
      setCollection([...collection]);
      Message.success('Added to collection!');
    },
    [collection, setCollection]
  );

  const removeCollection = useCallback(
    (id: string) => {
      if (!collection) return;

      collection[0].blocks = collection[0].blocks.filter(
        (item) => item.id !== id
      );
      setCollection([...collection]);
      Message.success('Remove collection');
    },
    [collection, setCollection]
  );

  const collectionCategory = useMemo((): BlockMarketCategory | null => {
    if (!collection) return null;
    const blockComponents = collection[0].blocks.map((item) => ({
      id: item.id,
      type: item.data.type,
      title: item.title,
      payload: item.data,
      description: item.description,
      thumbnail: item.thumbnail,
      component: () => (
        <BlockMaskWrapper
          key={item.id}
          type={item.data.type}
          payload={item.data}
        >
          <div style={{ position: 'relative' }}>
            <Picture src={item.thumbnail} />
            <div
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
                zIndex: 2,
              }}
            />
            <div
              onClick={() => removeCollection(item.id)}
              style={{
                position: 'absolute',
                top: 0,
                right: 0,
                transform: 'translate(27px, 35px)',
                zIndex: 3,
              }}
            >
              <IconFont iconName='icon-delete' />
            </div>
          </div>
        </BlockMaskWrapper>
      ),
    }));

    return {
      title: 'Collection',
      name: 'Collection',
      blocks: blockComponents,
    };
  }, [collection, removeCollection]);

  return {
    removeCollection,
    addCollection,
    collectionCategory,
  };
}
Example #13
Source File: CheckoutForm.tsx    From storefront with MIT License 4 votes vote down vote up
CheckoutForm: React.VFC<Props> = ({ cart, customer, loading, onSubmit }) => {
  const router = useRouter();

  const [creditCard, setCreditCard] = useState<{ cardType: string; lastFour: string }>();
  const [paymentMethod, setPaymentMethod] = useLocalStorage<string>('paymentMethod');
  const [paymentNonce, setPaymentNonce] = useState<string>();
  const [shipToDifferentAddress, setShipToDifferentAddress] = useState(
    !isShippingSameAsBilling(customer.shipping, customer.billing),
  );

  useEffect(() => {
    if (router.query.step === 'review' && paymentMethod === 'braintree_cc' && creditCard == null) {
      router.push('/checkout/billing-address');
    }
  }, [creditCard, paymentMethod, router]);

  /**
   * Handle changing steps.
   *
   * @param step The current step
   */
  const handleChangeStep = (step: Partial<{ path: string }>) => {
    // Everytime we change steps we have to check if both addresses match
    setShipToDifferentAddress(!isShippingSameAsBilling(customer.shipping, customer.billing));

    if (router.query.step != null && router.query.step !== step.path) {
      router.replace('/checkout/[step]', `/checkout/${step.path}`, { shallow: true });
    }
  };

  /**
   * Called when the user wants both addresses to be the same.
   *
   * @param shippingSameAsBilling Whether addresses should match or not
   */
  const handleShippingSameAsBillingChange = (shippingSameAsBilling: boolean) => {
    setShipToDifferentAddress(!shippingSameAsBilling);
  };

  /**
   * Handle submit of last step.
   */
  const handleSubmit = ({
    customerNote,
    metaData,
    transactionId,
  }: Pick<CheckoutMutationVariables, 'customerNote' | 'metaData' | 'transactionId'>) => {
    onSubmit({
      customerNote,
      metaData,
      paymentMethod,
      shipToDifferentAddress,
      transactionId,
    });
  };

  return (
    <Grid container spacing={4}>
      <Grid item xs={12} md={8}>
        <Stepper
          steps={[
            { path: 'billing-address', title: 'Billing Address' },
            { path: 'shipping-address', title: 'Shipping Address' },
            { path: 'shipping-options', title: 'Shipping Method' },
            { path: 'payment', title: 'Payment Method' },
            { path: 'review', title: 'Review' },
          ]}
          activeStep={`${router.query.step}`}
          onChangeStep={handleChangeStep}
        >
          {({ handleNext }) => [
            <Step key="billing-address">
              <BillingForm defaultValues={customer.billing} onSubmit={handleNext} />
            </Step>,
            <Step key="shipping-address">
              <ShippingForm
                defaultValues={customer.shipping}
                shipToDifferentAddress={shipToDifferentAddress}
                onShippingSameAsBillingChange={handleShippingSameAsBillingChange}
                onSubmit={handleNext}
              />
            </Step>,
            <Step key="shipping-options">
              <ShippingMethods
                availableShippingMethods={cart.availableShippingMethods}
                chosenShippingMethods={cart.chosenShippingMethods}
                onSubmit={handleNext}
              />
            </Step>,
            <Step key="payment">
              <PaymentMethods
                customer={customer}
                paymentMethod={paymentMethod}
                onSubmit={(values) => {
                  setPaymentMethod(values.paymentMethod);
                  setPaymentNonce(values.paymentNonce);
                  setCreditCard(values.creditCard);
                  handleNext();
                }}
              />
            </Step>,
            <Step key="review">
              <CheckoutReview
                cart={cart}
                creditCard={creditCard}
                customer={customer}
                loading={loading}
                paymentMethod={paymentMethod}
                paymentNonce={paymentNonce}
                onSubmit={handleSubmit}
              />
            </Step>,
          ]}
        </Stepper>
      </Grid>
      <Grid item xs={12} md={4}>
        <CartSummary cart={cart} />
      </Grid>
    </Grid>
  );
}
Example #14
Source File: App.tsx    From back-home-safe with GNU General Public License v3.0 4 votes vote down vote up
App = () => {
  useMigration();
  const [finishedTutorial, setFinishedTutorial] = useLocalStorage(
    "finished_tutorial",
    false
  );
  const [confirmPageIcon, setConfirmPageIcon] = useLocalStorage<string | null>(
    "confirmPageIcon",
    null
  );
  const { lockStore, unlocked, isEncrypted } = useData();

  const { pathname } = useLocation();
  const browserHistory = useHistory();

  const handleBlur = useCallback(() => {
    if (pathname !== "/qrReader" && pathname !== "/cameraSetting") lockStore();
  }, [lockStore, pathname]);

  useEffect(() => {
    window.addEventListener("blur", handleBlur);
    return () => {
      window.removeEventListener("blur", handleBlur);
    };
  }, [handleBlur]);

  const pageMap = useMemo<
    { route: RouteProps; component: React.ReactNode; privateRoute: boolean }[]
  >(
    () => [
      {
        privateRoute: false,
        route: { exact: true, path: "/tutorial" },
        component: <Tutorial setFinishedTutorial={setFinishedTutorial} />,
      },
      {
        privateRoute: false,
        route: { exact: true, path: "/login" },
        component: <Login />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/",
        },
        component: <MainScreen />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/confirm/:id",
        },
        component: <Confirm confirmPageIcon={confirmPageIcon} />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/qrGenerator",
        },
        component: <QRGenerator />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/disclaimer",
        },
        component: <Disclaimer />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/qrReader",
        },
        component: <QRReader />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/cameraSetting",
        },
        component: <CameraSetting />,
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/confirmPageSetting",
        },
        component: (
          <ConfirmPageSetting
            confirmPageIcon={confirmPageIcon}
            setConfirmPageIcon={setConfirmPageIcon}
          />
        ),
      },
      {
        privateRoute: true,
        route: {
          exact: true,
          path: "/vaccinationQRReader",
        },
        component: <VaccinationQRReader />,
      },
    ],
    [confirmPageIcon, setConfirmPageIcon, setFinishedTutorial]
  );

  // transition group cannot use switch component, thus need manual redirect handling
  // ref: https://reactcommunity.org/react-transition-group/with-react-router
  useEffect(() => {
    if (!unlocked && pathname !== "/login") {
      browserHistory.replace("/login");
    }
    if (unlocked && pathname === "/login") {
      browserHistory.replace("/");
    }
  }, [isEncrypted, unlocked, browserHistory, pathname]);

  useEffect(() => {
    if (!finishedTutorial && pathname !== "/tutorial") {
      browserHistory.replace("/tutorial");
    }
    if (finishedTutorial && pathname === "/tutorial") {
      browserHistory.replace("/");
    }
  }, [finishedTutorial, browserHistory, pathname]);

  useEffect(() => {
    const hasMatch = any(({ route }) => {
      if (!route.path) return false;
      return !isNil(matchPath(pathname, route));
    }, pageMap);

    if (!hasMatch) {
      browserHistory.replace("/");
    }
  }, [browserHistory, pathname, pageMap]);

  return (
    <>
      <GlobalStyle />
      {pageMap.map(({ route, component, privateRoute }) =>
        privateRoute && !unlocked ? (
          <React.Fragment key={String(route.path)} />
        ) : (
          <Route {...route} key={String(route.path)}>
            {({ match }) => (
              <CSSTransition
                in={match != null}
                timeout={300}
                classNames="page"
                unmountOnExit
              >
                <div className="page">
                  <Suspense fallback={<PageLoading />}>{component}</Suspense>
                </div>
              </CSSTransition>
            )}
          </Route>
        )
      )}
    </>
  );
}
Example #15
Source File: useEncryptedStore.ts    From back-home-safe with GNU General Public License v3.0 4 votes vote down vote up
useEncryptedStore = <T extends T[] | Object>({
  key,
  defaultValue: fallbackValue,
  passthroughOnly = false,
}: {
  key: string;
  defaultValue: T;
  passthroughOnly?: boolean;
}) => {
  const [incognito, setIncognito] = useLocalStorage("incognito", false);
  const [defaultValue] = useState<T>(fallbackValue);

  const [password, setPassword] = useState<string | null>(null);
  const [savedValue, setSavedValue, removeSavedValue] = useLocalStorage<string>(
    key,
    passthroughOnly
      ? undefined
      : password
      ? encryptValue(JSON.stringify(defaultValue), password)
      : JSON.stringify(defaultValue)
  );

  const isEncrypted = useMemo(() => {
    try {
      if (!savedValue) return false;
      JSON.parse(savedValue);
    } catch (e) {
      return true;
    }
    return false;
  }, [savedValue]);

  const [decryptedValue, setDecryptedValue] = useState<T>(
    !isEncrypted && savedValue ? JSON.parse(savedValue) : defaultValue
  );

  const [unlocked, setUnlocked] = useState(!isEncrypted);

  useDeepCompareEffect(() => {
    if (!unlocked || incognito || passthroughOnly) return;
    if (isEncrypted && !password) return;
    const value = JSON.stringify(decryptedValue);
    console.log(value);
    setSavedValue(password ? encryptValue(value, password) : value);
  }, [decryptedValue]);

  const initPassword = useCallback(
    (newPassword: string) => {
      if (isEncrypted || passthroughOnly) return;
      const data = encryptValue(
        savedValue || JSON.stringify(defaultValue),
        newPassword
      );
      setSavedValue(data);
      setPassword(newPassword);
    },
    [passthroughOnly, savedValue, setSavedValue, defaultValue, isEncrypted]
  );

  const unlockStore = useCallback(
    (password: string) => {
      if (!isEncrypted) return true;
      try {
        const decryptedValue = decryptValue(
          savedValue || JSON.stringify(defaultValue),
          password
        );
        setDecryptedValue(JSON.parse(decryptedValue));
        setPassword(password);
        setUnlocked(true);

        return true;
      } catch (e) {
        return false;
      }
    },
    [defaultValue, savedValue, isEncrypted]
  );

  const lockStore = useCallback(() => {
    if (!isEncrypted) return;
    setUnlocked(false);
    setPassword(null);
    setDecryptedValue(defaultValue);
  }, [isEncrypted, defaultValue]);

  return {
    isEncrypted,
    unlockStore,
    lockStore,
    value: decryptedValue,
    setValue: setDecryptedValue,
    initPassword,
    unlocked,
    incognito,
    setIncognito,
    password,
    destroy: removeSavedValue,
    hasData: !isNil(savedValue),
  };
}
Example #16
Source File: useTravelRecord.ts    From back-home-safe with GNU General Public License v3.0 4 votes vote down vote up
[UseTravelRecordProvider, useTravelRecord] = constate(() => {
  const { currentTime } = useTime();
  const {
    value: { travelRecords },
    setValue,
  } = useData();
  const browserHistory = useHistory();

  const [autoRemoveRecordDay, setAutoRemoveRecordDay] = useLocalStorage(
    "auto_remove_record_after",
    30
  );

  const { pastTravelRecord, currentTravelRecord } = useMemo(() => {
    const { pastTravelRecord, currentTravelRecord } = travelRecords.reduce<{
      pastTravelRecord: TravelRecord[];
      currentTravelRecord: TravelRecord[];
    }>(
      (acc, item) => {
        const isPast =
          item.outTime && dayjs(item.outTime).isBefore(currentTime);

        return isPast
          ? {
              pastTravelRecord: [item, ...acc.pastTravelRecord],
              currentTravelRecord: [...acc.currentTravelRecord],
            }
          : {
              pastTravelRecord: [...acc.pastTravelRecord],
              currentTravelRecord: [item, ...acc.currentTravelRecord],
            };
      },
      {
        pastTravelRecord: [],
        currentTravelRecord: [],
      }
    );

    return {
      pastTravelRecord: sortRecord(pastTravelRecord),
      currentTravelRecord: sortRecord(currentTravelRecord),
    };
  }, [travelRecords, currentTime]);

  useEffect(() => {
    setValue((prev) => ({
      ...prev,
      travelRecords: prev.travelRecords.filter(
        ({ inTime }) =>
          currentTime.diff(inTime, "day") <= (autoRemoveRecordDay || 30)
      ),
    }));
  }, [currentTime, setValue, autoRemoveRecordDay]);

  const createTravelRecord = useCallback(
    (record: TravelRecord) => {
      setValue((prev) => ({
        ...prev,
        travelRecords: [record, ...prev.travelRecords],
      }));
    },
    [setValue]
  );

  const getTravelRecord = useCallback(
    (id: string) => find(({ id: itemId }) => itemId === id, travelRecords),
    [travelRecords]
  );

  const updateTravelRecord = useCallback(
    (id: string, data: Partial<TravelRecord>) => {
      setValue((prev) => {
        const index = findIndex(
          ({ id: itemId }) => itemId === id,
          prev.travelRecords
        );
        if (index < 0) return prev;
        return {
          ...prev,
          travelRecords: adjust(
            index,
            (currentRecord) => ({ ...currentRecord, ...data }),
            prev.travelRecords
          ),
        };
      });
    },
    [setValue]
  );

  const removeTravelRecord = useCallback(
    (id: string) => {
      setValue((prev) => ({
        ...prev,
        travelRecords: reject(
          ({ id: itemId }) => itemId === id,
          prev.travelRecords
        ),
      }));
    },
    [setValue]
  );

  const enterLocation = useCallback(
    (location: Location & { inputType: travelRecordInputType }) => {
      const id = uuid();
      const now = dayjs();

      const record = {
        ...location,
        id,
        inTime: now.toISOString(),
        outTime: now.add(4, "hour").toISOString(),
      };

      createTravelRecord(record);

      browserHistory.push({ pathname: `/confirm/${id}`, state: record });
    },
    [createTravelRecord, browserHistory]
  );

  return {
    travelRecords,
    currentTravelRecord,
    pastTravelRecord,
    createTravelRecord,
    getTravelRecord,
    updateTravelRecord,
    removeTravelRecord,
    setAutoRemoveRecordDay,
    autoRemoveRecordDay,
    enterLocation,
  };
})