From e511fccd2cde64c03d96b52144fd15ee425a498d Mon Sep 17 00:00:00 2001 From: Gince Date: Sun, 5 May 2024 05:30:16 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20hooks=20and=20provide?= =?UTF-8?q?r=20for=20Appwrite=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces new hooks and a provider component to manage Appwrite authentication and data fetching. The `useFetchData` hook simplifies the process of fetching data from Appwrite, including loading and error state management. The `AppWriteProvider` component provides context for Appwrite authentication, including sign-up, sign-in, and sign-out functionality, as well as user state management. Changes include: - `useFetchData` hook for data fetching with loading and error handling. - `AppWriteProvider` component for managing Appwrite authentication context. - Exporting new hooks and provider from the main entry point. - TypeScript definitions for the new hooks and provider. - Updated `tsconfig.json` to support JSX due to the new React component. - Adjustments to type definitions to align with the new features. --- src/hooks/useFetchData.tsx | 34 ++++++++++ src/index.ts | 1 + src/lib/AppWriteProvider.tsx | 75 +++++++++++++++++++++ src/lib/app-write-auth.ts | 113 ++++++++++++++++++++++++++++++++ tsconfig.json | 1 + types/hooks/useFetchData.d.ts | 7 ++ types/index.d.ts | 1 + types/lib/AppWriteProvider.d.ts | 24 +++++++ types/lib/app-write-auth.d.ts | 8 +++ 9 files changed, 264 insertions(+) create mode 100644 src/hooks/useFetchData.tsx create mode 100644 src/lib/AppWriteProvider.tsx create mode 100644 src/lib/app-write-auth.ts create mode 100644 types/hooks/useFetchData.d.ts create mode 100644 types/lib/AppWriteProvider.d.ts create mode 100644 types/lib/app-write-auth.d.ts diff --git a/src/hooks/useFetchData.tsx b/src/hooks/useFetchData.tsx new file mode 100644 index 0000000..bbd3da0 --- /dev/null +++ b/src/hooks/useFetchData.tsx @@ -0,0 +1,34 @@ +import { useCallback, useEffect, useState } from 'react'; + +// Assuming the function passed to useAppwrite returns a Promise of type T +const useFetchData = (function_: () => Promise) => { + // State to store data with initial empty array, typed as T[] + const [data, setData] = useState(); + const [error, setError] = useState(); + // State to store loading status, typed as boolean + const [isLoading, setIsLoading] = useState(true); + + // Memoize fetchData using useCallback to prevent redefinition unless function_ changes + const fetchData = useCallback(async () => { + setIsLoading(true); + try { + const response = await function_(); + setData(response); + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }, [function_]); // Dependency array includes function_ + + useEffect(() => { + void fetchData(); + }, [fetchData]); + + const refetch = async () => fetchData(); + + // Return type explicitly typed with the structure of the returned object + return { data, isLoading, error, refetch }; +}; + +export default useFetchData; diff --git a/src/index.ts b/src/index.ts index bbc74cc..9623b2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,3 +23,4 @@ export { Flag } from './enums/flag'; export { ExecutionMethod } from './enums/execution-method'; export { ImageGravity } from './enums/image-gravity'; export { ImageFormat } from './enums/image-format'; +export { useAppWrite, AppWriteProvider } from './lib/AppWriteProvider'; diff --git a/src/lib/AppWriteProvider.tsx b/src/lib/AppWriteProvider.tsx new file mode 100644 index 0000000..dde7d29 --- /dev/null +++ b/src/lib/AppWriteProvider.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { type Models } from '../index'; +import { createContext, type FC, useContext, useEffect, useMemo, useState } from 'react'; +import { appWriteAuth } from './app-write-auth'; + +export type AppwriteConfigType = { + readonly endpoint: string; + readonly platform: string; + readonly projectId: string; + readonly databaseId: string; + readonly userCollectionId: string; // 'users' +}; + +export type AppWriteProviderProps = { + readonly children: React.ReactNode; + readonly config: AppwriteConfigType; +}; + +export type AppWriteContextType = { + signUp: (email: string, password: string, username: string) => Promise; + signIn: (email: string, password: string) => Promise; + signOut: () => Promise<{}>; + isAuthenticated: boolean; + user: Models.Document | undefined; + isAuthenticationLoading: boolean; +}; + +const AppWriteContext = createContext(undefined); + +export const useAppWrite = () => useContext(AppWriteContext); + +export const AppWriteProvider: FC = ({ children, config }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(undefined); + const [isAuthenticationLoading, setIsAuthenticationLoading] = useState(true); + + const { signUp, signIn, signOut, getCurrentUser } = appWriteAuth(config); + + const fetchUserData = async () => { + setIsAuthenticationLoading(true); + const userData = await getCurrentUser(); + if (userData) { + setIsAuthenticated(true); + setUser(userData); + } else { + setIsAuthenticated(false); + setUser(undefined); + } + setIsAuthenticationLoading(false); + }; + + const handleSignIn = async (email: string, password: string) => { + const session = await signIn(email, password); + await fetchUserData(); // Refresh user data post-sign-in + return session; + }; + + const contextValue = useMemo( + () => ({ + signUp, + signIn: handleSignIn, + signOut, + isAuthenticated, + user, + isAuthenticationLoading, + }), + [isAuthenticated, user, isAuthenticationLoading] + ); + + useEffect(() => { + void fetchUserData(); + }, []); + + return {children}; +}; diff --git a/src/lib/app-write-auth.ts b/src/lib/app-write-auth.ts new file mode 100644 index 0000000..4c734a6 --- /dev/null +++ b/src/lib/app-write-auth.ts @@ -0,0 +1,113 @@ +import { + Account, + Avatars, + Client, + Databases, + ID, + Query, +} from '../index'; + +import { type AppwriteConfigType } from './AppWriteProvider'; + +function handleApiError(error: unknown): Error { + if (error instanceof Error) { + return new Error(error.message); + } + return new Error('An unexpected error occurred'); +} + +export const appWriteAuth = (config: AppwriteConfigType) => { + const client = new Client(); + + client.setEndpoint(config.endpoint).setProject(config.projectId).setPlatform(config.platform); + + const account = new Account(client); + const avatars = new Avatars(client); + const databases = new Databases(client); + + // Register user + const signUp = async (email: string, password: string, username: string) => { + try { + const newAccount = await account.create(ID.unique(), email, password, username); + + const avatarUrl = avatars.getInitials(username); + + const newUser = await databases.createDocument( + config.databaseId, + config.userCollectionId, + ID.unique(), + { + accountId: newAccount.$id, + email, + username, + avatar: avatarUrl, + } + ); + + await signIn(email, password); + + return newUser; + } catch (error: unknown) { + throw handleApiError(error); + } + }; + + // Sign In + const signIn = async (email: string, password: string) => { + try { + const session = await account.createEmailPasswordSession(email, password); + + return session; + } catch (error) { + throw handleApiError(error); + } + }; + + // Sign Out + const signOut = async () => { + try { + const session = await account.deleteSession('current'); + + return session; + } catch (error) { + throw handleApiError(error); + } + }; + + // Get Account + const getAccount = async () => { + try { + const currentAccount = await account.get(); + + return currentAccount; + } catch (error) { + throw handleApiError(error); + } + }; + + // Get Current User + const getCurrentUser = async () => { + try { + const currentAccount = await getAccount(); + + const currentUser = await databases.listDocuments( + config.databaseId, + config.userCollectionId, + [Query.equal('accountId', currentAccount.$id)] + ); + + return currentUser.documents[0]; + } catch (error) { + console.log(error); + return null; + } + }; + + return { + signUp, + signIn, + getAccount, + getCurrentUser, + signOut, + }; +}; diff --git a/tsconfig.json b/tsconfig.json index 68f10f0..5be5633 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "esModuleInterop": true, "inlineSourceMap": false, "lib": ["ESNext", "DOM"], + "jsx": "react", "listEmittedFiles": false, "listFiles": false, "moduleResolution": "node", diff --git a/types/hooks/useFetchData.d.ts b/types/hooks/useFetchData.d.ts new file mode 100644 index 0000000..4178613 --- /dev/null +++ b/types/hooks/useFetchData.d.ts @@ -0,0 +1,7 @@ +declare const useFetchData: (function_: () => Promise) => { + data: T | undefined; + isLoading: boolean; + error: string | undefined; + refetch: () => Promise; +}; +export default useFetchData; diff --git a/types/index.d.ts b/types/index.d.ts index bbc74cc..9623b2d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -23,3 +23,4 @@ export { Flag } from './enums/flag'; export { ExecutionMethod } from './enums/execution-method'; export { ImageGravity } from './enums/image-gravity'; export { ImageFormat } from './enums/image-format'; +export { useAppWrite, AppWriteProvider } from './lib/AppWriteProvider'; diff --git a/types/lib/AppWriteProvider.d.ts b/types/lib/AppWriteProvider.d.ts new file mode 100644 index 0000000..0201760 --- /dev/null +++ b/types/lib/AppWriteProvider.d.ts @@ -0,0 +1,24 @@ +import React from 'react'; +import { type Models } from '../index'; +import { type FC } from 'react'; +export declare type AppwriteConfigType = { + readonly endpoint: string; + readonly platform: string; + readonly projectId: string; + readonly databaseId: string; + readonly userCollectionId: string; +}; +export declare type AppWriteProviderProps = { + readonly children: React.ReactNode; + readonly config: AppwriteConfigType; +}; +export declare type AppWriteContextType = { + signUp: (email: string, password: string, username: string) => Promise; + signIn: (email: string, password: string) => Promise; + signOut: () => Promise<{}>; + isAuthenticated: boolean; + user: Models.Document | undefined; + isAuthenticationLoading: boolean; +}; +export declare const useAppWrite: () => AppWriteContextType | undefined; +export declare const AppWriteProvider: FC; diff --git a/types/lib/app-write-auth.d.ts b/types/lib/app-write-auth.d.ts new file mode 100644 index 0000000..417c65a --- /dev/null +++ b/types/lib/app-write-auth.d.ts @@ -0,0 +1,8 @@ +import { type AppwriteConfigType } from './AppWriteProvider'; +export declare const appWriteAuth: (config: AppwriteConfigType) => { + signUp: (email: string, password: string, username: string) => Promise; + signIn: (email: string, password: string) => Promise; + getAccount: () => Promise>; + getCurrentUser: () => Promise; + signOut: () => Promise<{}>; +};