UNPKG

react-playmakers

Version:

React wrapper providing utilities for PlayMakers integration

1 lines 277 kB
{"version":3,"file":"index.cjs","sources":["../src/api/Requests.tsx","../src/Api.ts","../src/PlayMakersProvider.tsx","../src/utils/providers.tsx","../src/AuthContext.tsx","../src/cache.ts","../src/hooks/usePagination.ts","../src/hooks/useAsset.ts","../src/hooks/useSchema.ts","../src/types/comment.types.ts","../src/hooks/useBarista.ts","../src/hooks/useComment.ts","../src/hooks/useIdServer.ts","../src/hooks/useRewardType.ts","../src/hooks/useSubmission.ts","../src/hooks/useUserQuest.ts","../src/hooks/useUserReward.ts","../src/hooks/useCategory.ts","../src/hooks/useNotification.ts","../src/hooks/useInterval.ts","../src/hooks/useProject.ts","../src/hooks/useQuest.ts","../src/hooks/useQuestType.ts","../src/hooks/useReward.ts","../src/hooks/useTag.ts","../src/hooks/useUser.ts","../src/hooks/useVote.ts"],"sourcesContent":["import {fetchAuthSession} from \"aws-amplify/auth\";\nimport axios, {AxiosError, AxiosRequestConfig} from \"axios\";\n\n\nasync function getAccessToken(forceRefresh: boolean = false): Promise<string | null> {\n try {\n const {accessToken} = (await fetchAuthSession({forceRefresh})).tokens ?? {};\n if(!accessToken || !accessToken?.toString()) return null;\n // Force refresh if the token is about to expire (5 mins)\n if(!forceRefresh && accessToken?.payload?.exp) {\n const expiration = accessToken.payload.exp * 1000;\n const now = (new Date).getTime();\n const fiveMinutes = 5 * 60 * 1000;\n if(expiration - now < fiveMinutes)\n return await getAccessToken(true);\n }\n return accessToken?.toString();\n } catch (error) {\n return null;\n }\n}\n\n// Redirect as POST to ID Server, with access token as parameter\nexport async function redirectAsPost(url: string, params: any = {}): Promise<void> {\n params.accessToken = await getAccessToken();\n const form = document.createElement(\"form\");\n form.method = \"POST\";\n form.action = url;\n\n Object.keys(params).forEach(key => {\n const hiddenField = document.createElement(\"input\");\n hiddenField.type = \"hidden\";\n hiddenField.name = key;\n hiddenField.value = params[key];\n form.appendChild(hiddenField);\n });\n\n document.body.appendChild(form);\n form.submit();\n}\n\n\n// Global cache for all API requests\n// getCache and setCache are used only internally to the senRequest functions.\nconst cache: any = {};\nconst getCache = async(url: string, method: string = \"\", param: any = \"\", cacheExpiration: number = 2000) => {\n const key = `${url}(${method}){${JSON.stringify(param)}}`;\n const cacheData = cache[key];\n // If the method is not get, we use this to prevent spamming\n if(method !== \"get\") cacheExpiration = cacheExpiration < 500 ? cacheExpiration : 500;\n if(cacheData && (new Date).getTime() - cacheData.timestamp < cacheExpiration)\n return (await cacheData.response)?.data;\n return undefined;\n};\nconst setCache = (url: string, method: string = \"\", param: any = \"\", response: any) => {\n const key = `${url}(${method}){${JSON.stringify(param)}}`;\n cache[key] = {\n response,\n timestamp: (new Date).getTime()\n };\n};\n\n\nexport interface AuthRequestProps {\n cacheExpiration?: number;\n url: string;\n method?: \"get\" | \"post\" | \"put\" | \"delete\";\n params?: Record<string, any>;\n accessToken?: string,\n refreshFails?: number,\n forceRefresh?: boolean,\n files?: File[]\n}\nexport async function sendAuthenticatedRequest<T>({\n cacheExpiration,\n url,\n method,\n params = {},\n files = []\n}: AuthRequestProps): Promise<T> {\n const cacheData = await getCache(url, method, params, cacheExpiration);\n if(cacheData !== undefined) return cacheData;\n\n\n const config: AxiosRequestConfig = {\n params: method === \"get\" ? params : undefined\n };\n config.headers= {};\n\n let data = params;\n\n if(files && files.length) {\n data = new FormData;\n Object.entries(params).forEach(([key, value]) => {\n data.append(key, value);\n });\n config.headers[\"Content-Type\"] = \"multipart/form-data\";\n files.forEach(file => {\n data.append(\"files\", file);\n });\n } else\n config.headers[\"Content-Type\"] = \"application/json\";\n\n const accessToken = await getAccessToken();\n if(accessToken)\n config.headers.Authorization = `Bearer ${accessToken}`;\n\n\n try {\n const apiCall = axios({\n data,\n method,\n url,\n ...config\n });\n setCache(url, method, params, apiCall);\n const response = await apiCall;\n return response.data;\n } catch (error: unknown) {\n const status = (error as AxiosError).response?.status || 400;\n const {code} = (error as AxiosError);\n\n console.error(\"Connection error\", code, status);\n throw (error as AxiosError).response?.data || error;\n }\n}\n","import {Amplify} from \"aws-amplify\";\n\nimport {AuthRequestProps, sendAuthenticatedRequest} from \"./api/Requests\";\nimport {\n AssetPayloadType,\n AssetType,\n CategoryParentType,\n CategoryPostResponseType,\n CategoryType,\n CommentType,\n CreateQuest,\n CreateReward,\n NotificationType,\n PlayMakersConfig,\n ProjectConfigType,\n ProjectPayloadType,\n ProjectPushPayloadType,\n ProjectType,\n Quest,\n QuestType,\n QuestWithQuestType,\n Reward,\n RewardType,\n SchemaPayloadType,\n SchemaType,\n SelfUser,\n SubmissionType,\n SubmissionVoteGetResponseType,\n UpdateQuest,\n UpdateReward,\n User,\n UserCreateParentResponse,\n UserLinkFederatedResponse,\n UserQuest,\n UserReward,\n UserUpdateAvatarResponse,\n VoteGetResponseType,\n VotePostResponseType} from \"./types\";\n\nclass Api {\n baseUrl;\n cacheExpiration;\n uploadsUrl;\n projectId;\n twitterSignInUrl;\n\n constructor(config: PlayMakersConfig) {\n this.baseUrl = config.API_BASEURL;\n this.cacheExpiration = config.API_CACHE_EXPIRATION;\n this.uploadsUrl = config.UPLOADS_BASEURL;\n this.projectId = config.PROJECT_ID;\n const redirectUrlParam = encodeURIComponent(config.TWITTER.CALLBACK_URL);\n this.twitterSignInUrl = `${this.baseUrl}/auth/redirect/twitter?redirectUrl=${redirectUrlParam}`;\n Amplify.configure({\n Auth: {\n Cognito: {\n loginWith: {\n oauth: {\n domain: config.COGNITO.DOMAIN,\n redirectSignIn: config.COGNITO.REDIRECT_SIGNIN,\n redirectSignOut: config.COGNITO.REDIRECT_SIGNOUT,\n responseType: \"code\",\n // Amplify Bug as of 202405- scope is fixed as aws.cognito.signin.user.admin\n // for all users. We therefore ignore scope for authorization.\n // https://github.com/aws-amplify/amplify-js/issues/3732\n scopes: [\"aws.cognito.signin.user.admin\", \"email\", \"profile\", \"openid\"]\n },\n username: true\n },\n userPoolClientId: config.COGNITO.CLIENT_ID,\n userPoolId: config.COGNITO.POOL_ID\n }\n }\n });\n }\n\n call = async<T = void>({url, ...rest}: AuthRequestProps) => await sendAuthenticatedRequest<T>({\n cacheExpiration: this.cacheExpiration,\n url: `${this.baseUrl}/${url}`,\n ...rest\n });\n\n exports = [\n \"baristaRequest\",\n \"createAsset\",\n \"updateAsset\",\n \"updateAssetFile\",\n \"getAsset\",\n \"getGlobalAssets\",\n \"getProjectAssets\",\n \"getSchemaAssets\",\n \"deleteAsset\",\n \"getCategories\",\n \"getParentCategories\",\n \"createCategory\",\n \"deleteCategory\",\n \"renameCategory\",\n \"getUserComments\",\n \"getSubmissionComments\",\n \"getComment\",\n \"createComment\",\n \"updateComment\",\n \"deleteComment\",\n \"getProjects\",\n \"getProject\",\n \"createProject\",\n \"updateProject\",\n \"updateProjectConfig\",\n \"updateProjectThumbnail\",\n \"createSchema\",\n \"updateSchema\",\n \"deleteSchema\",\n \"getSchemas\",\n \"getSchemasByCategory\",\n \"getSchema\",\n \"updateSchemaThumbnail\",\n \"createSubmission\",\n \"updateSubmission\",\n \"deleteSubmission\",\n \"getSubmission\",\n \"getSubmissions\",\n \"getSubmissionsBySchema\",\n \"getSubmissionsByIds\",\n \"updateSubmissionsStates\",\n \"signUp\",\n \"signUpConfirmation\",\n \"resendConfirmationCode\",\n \"sendPasswordResetCode\",\n \"passwordReset\",\n \"getMe\",\n \"getUser\",\n \"getUserMetadata\",\n \"getTokensFromTwitterToken\",\n \"getCreators\",\n \"updateAvatar\",\n \"createUserParent\",\n \"linkFederated\",\n \"unlinkFederated\",\n \"vote\",\n \"downVote\",\n \"upVote\",\n \"cancelVote\",\n \"getVote\",\n \"voteBalance\",\n \"getNotifications\",\n \"markAllNotificationsAsRead\",\n \"updateNotification\",\n \"deleteNotification\",\n \"getTags\",\n \"redirectTwitterLogin\",\n \"getSteamProfile\",\n \"getQuestType\",\n \"getQuestTypes\",\n \"getQuestTypeCategories\",\n \"getQuests\",\n \"getQuest\",\n \"createQuest\",\n \"deleteQuest\",\n \"updateQuest\",\n \"getUserQuest\",\n \"getUserQuests\",\n \"getUserQuestsByProject\",\n \"getUserQuestsByQuest\",\n \"completeQuest\",\n \"getRewardTypes\",\n \"getRewardType\",\n \"getRewardsByProject\",\n \"getReward\",\n \"createReward\",\n \"updateReward\",\n \"deleteReward\",\n \"redeemReward\",\n \"getUserRewards\",\n \"getUserRewardsByProject\",\n \"getUserRewardsByReward\",\n \"getUserReward\",\n \"updateUserRewardState\"\n ];\n\n // ------------- HELPERS -------------\n\n extendSubmission = (submission: SubmissionType): SubmissionType => {\n if(!submission.zip) submission.zip = `${this.uploadsUrl}/${submission.id}/submission.zip`;\n return submission;\n };\n\n // ------------- MISC -------------\n\n baristaRequest = async({schema, submission}: any): Promise<boolean> =>\n await this.call<boolean>({\n method: \"post\",\n params: {schema, submission},\n url: \"barista\"\n });\n\n // ------------- ASSET -------------\n\n createAsset = async(params: Partial<AssetPayloadType>): Promise<AssetType> =>\n await this.call<AssetType>({\n method: \"post\",\n params,\n url: \"asset\"\n });\n\n updateAsset = async(assetId: string, params: Partial<AssetPayloadType>): Promise<AssetType> =>\n await this.call<AssetType>({\n method: \"put\",\n params,\n url: `asset/${assetId}`\n });\n\n updateAssetFile = async(assetId: string, file: File): Promise<AssetType> =>\n await this.call<AssetType>({\n files: [file],\n method: \"put\",\n url: `asset/${assetId}/upload`\n });\n\n getAsset = async(assetId: string): Promise<AssetType> =>\n await this.call<AssetType>({\n method: \"get\",\n url: `asset/${assetId}`\n });\n\n // params can have `type` and `tags`\n getGlobalAssets = async(params: {[key: string]: unknown}): Promise<AssetType[]> =>\n await this.call<AssetType[]>({\n method: \"get\",\n params,\n url: \"assets/global\"\n });\n\n // params can have `type` and `tags`\n getProjectAssets =\n async(params: {[key: string]: unknown}, projectId: string = this.projectId): Promise<AssetType[]> =>\n await this.call<AssetType[]>({\n method: \"get\",\n params,\n url: `assets/by/project/${projectId}`\n });\n\n // params can have `type` and `matchTags` <-- boolean if true looks into project and global\n getSchemaAssets = async(schemaId: string, params: {[key: string]: unknown} = {}): Promise<AssetType[]> =>\n await this.call<AssetType[]>({\n method: \"get\",\n params,\n url: `assets/by/schema/${schemaId}`\n });\n\n deleteAsset = async(assetId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `asset/${assetId}`\n });\n\n\n // ------------- CATEGORY -------------\n\n getCategories = async(\n parentId?: string, projectId: string = this.projectId, params:{[key: string]: unknown} = {}\n ): Promise<CategoryType[]> =>\n await this.call<CategoryType[]>({\n method: \"get\",\n params: {categoryId: parentId, ...params},\n url: `categories/${projectId}`\n });\n\n getParentCategories = async(categoryId: string): Promise<CategoryParentType[]> =>\n await this.call<CategoryParentType[]>({\n method: \"get\",\n url: `category/${categoryId}`\n });\n\n createCategory = async(\n name: string, parentId?: string, projectId: string = this.projectId\n ): Promise<CategoryPostResponseType> =>\n await this.call<CategoryPostResponseType>({\n method: \"post\",\n params: {name, parentId, projectId},\n url: \"category\"\n });\n\n deleteCategory = async(categoryId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `category/${categoryId}`\n });\n\n renameCategory = async(categoryId: string, name: string): Promise<CategoryPostResponseType> =>\n await this.call<CategoryPostResponseType>({\n method: \"put\",\n params: {name},\n url: `category/${categoryId}`\n });\n\n // ------------- COMMENT -------------\n\n // Get all comments written by a given user\n getUserComments = async(owner: string, params: {[key:string]: unknown}): Promise<CommentType[]> =>\n await this.call<CommentType[]>({\n method: \"get\",\n params,\n url: `comments/by/user/${owner}`\n });\n\n // Get all comments linked to a given submission\n getSubmissionComments = async(submissionId: string, params: {[key:string]: unknown}): Promise<CommentType[]> =>\n await this.call<CommentType[]>({\n method: \"get\",\n params,\n url: `comments/${submissionId}`\n });\n\n // Get a comment given its id\n getComment = async(id: string): Promise<CommentType> =>\n await this.call<CommentType>({\n method: \"get\",\n url: `comment/${id}`\n });\n\n // Create a new comment\n createComment = async(submissionId: string, body?: string, state?: string): Promise<CommentType> =>\n await this.call<CommentType>({\n method: \"post\",\n params: {body, state},\n url: `comment/${submissionId}`\n });\n\n // Update an existing comment\n updateComment = async(id:string, body?: string, state?: string): Promise<CommentType> =>\n await this.call<CommentType>({\n method: \"put\",\n params: {body, state},\n url: `comment/${id}`\n });\n\n // Delete an existing comment\n deleteComment = async(id:string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `comment/${id}`\n });\n\n // ------------- PROJECT -------------\n\n getProjects = async(params: {[key: string]: unknown} = {}): Promise<ProjectType[]> =>\n await this.call<ProjectType[]>({\n method: \"get\",\n params,\n url: \"projects\"\n });\n\n getProject = async(projectId: string = this.projectId): Promise<ProjectType> =>\n await this.call<ProjectType>({\n method: \"get\",\n url: `project/${projectId}`\n });\n\n createProject = async(projectParams: ProjectPayloadType): Promise<ProjectType> =>\n await this.call<ProjectType>({\n method: \"post\",\n params: projectParams,\n url: \"project\"\n });\n\n updateProject =\n async(params: Partial<ProjectPushPayloadType>, projectId: string = this.projectId): Promise<ProjectType> =>\n await this.call<ProjectType>({\n method: \"put\",\n params,\n url: `project/${projectId}`\n });\n\n updateProjectConfig = async(config: Partial<ProjectConfigType>, projectId: string = this.projectId): Promise<void> =>\n await this.call<void>({\n method: \"put\",\n params: config,\n url: `project/${projectId}/config`\n });\n\n\n updateProjectThumbnail = async(file: File, projectId: string = this.projectId): Promise<ProjectType> =>\n await this.call<ProjectType>({\n files: [file],\n method: \"put\",\n url: `project/${projectId}/thumbnail`\n });\n\n // ------------- SCHEMA -------------\n\n createSchema = async(params: SchemaPayloadType): Promise<SchemaType> =>\n await this.call<SchemaType>({\n method: \"post\",\n params,\n url: \"schema\"\n });\n\n updateSchema = async(schemaId: string, params: Partial<SchemaPayloadType>): Promise<SchemaType> =>\n await this.call<SchemaType>({\n method: \"put\",\n params,\n url: `schema/${schemaId}`\n });\n\n getSchemas =\n async(projectId: string = this.projectId, params: {[key: string]: unknown} = {}): Promise<SchemaType[]> =>\n await this.call<SchemaType[]>({\n method: \"get\",\n params,\n url: `schemas/by/project/${projectId}`\n });\n\n // TODO Is this needed ? Could have category as param of above\n getSchemasByCategory = async(categoryId: string): Promise<SchemaType[]> =>\n await this.call<SchemaType[]>({\n method: \"get\",\n url: `schemas/by/category/${categoryId}`\n });\n\n getSchema = async(schemaId: string): Promise<SchemaType> =>\n await this.call<SchemaType>({\n method: \"get\",\n url: `schema/${schemaId}`\n });\n\n deleteSchema = async(schemaId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `schema/${schemaId}`\n });\n\n updateSchemaThumbnail = async(schemaId: string, file: File): Promise<SchemaType> =>\n await this.call<SchemaType>({\n files: [file],\n method: \"put\",\n url: `schema/${schemaId}/thumbnail`\n });\n\n // ------------- SUBMISSION -------------\n\n createSubmission = async(params: {[key: string]: unknown} = {}, files: File[] = []): Promise<SubmissionType> =>\n this.extendSubmission(await this.call<SubmissionType>({\n files,\n method: \"post\",\n params,\n url: \"submission\"\n }));\n\n updateSubmission =\n async(submissionId: string, params: {[key: string]: unknown} = {}, files: File[] = []): Promise<SubmissionType> =>\n this.extendSubmission(await this.call<SubmissionType>({\n files,\n method: \"put\",\n params,\n url: `submission/${submissionId}`\n }));\n\n getSubmission = async(submissionId: string): Promise<SubmissionType> =>\n this.extendSubmission(await this.call<SubmissionType>({\n method: \"get\",\n url: `submission/${submissionId}`\n }));\n\n deleteSubmission = async(submissionId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `submission/${submissionId}`\n });\n\n getSubmissions = async(params: {[key: string]: unknown} = {}): Promise<SubmissionType[]> => {\n const submissions = await this.call<SubmissionType[]>({\n method: \"get\",\n params,\n url: \"submissions\"\n });\n return submissions.map(x => this.extendSubmission(x));\n };\n\n\n getSubmissionsBySchema = async(schemaId: string): Promise<SubmissionType[]> => {\n const submissions = await this.call<SubmissionType[]>({\n method: \"get\",\n params: {schemaId},\n url: `submissions/by/schema/${schemaId}`\n });\n return submissions.map(x => this.extendSubmission(x));\n };\n\n getSubmissionsByIds = async(submissionIds: string[]): Promise<SubmissionType[]> => {\n const submissions = await this.call<SubmissionType[]>({\n method: \"get\",\n params: {submissionIds},\n url: \"submissions/by/ids\"\n });\n return submissions.map(x => this.extendSubmission(x));\n };\n\n // TODO state should be of SubmissionState type\n updateSubmissionsStates = async(submissionIds: string[], state: string): Promise<void> =>\n await this.call<void>({\n method: \"put\",\n params: {state, submissionIds},\n url: \"submissions/by/ids\"\n });\n\n // ------------- USER -------------\n\n // Signup non-confirmed new user, receives code (via email)\n signUp = async(email: string, username: string, password: string): Promise<void> =>\n await this.call<void>({\n method: \"post\",\n params: {email, password, username},\n url: \"user/auth/signup\"\n });\n\n // Resend confirmation code for non-confirmed user (via email or username)\n resendConfirmationCode = async(email: string | null, username: string | null): Promise<void> => {\n if(!email && !username) throw new Error(\"Either email or username must be provided.\");\n\n const params = {...email && {email}, ...username && {username}};\n\n return await this.call<void>({\n method: \"post\",\n params,\n url: \"user/auth/resendConfirmation\"\n });\n };\n\n // User requests code to reset password (via email)\n sendPasswordResetCode = async(email: string): Promise<void> =>\n await this.call<void>({\n method: \"post\",\n params: {email},\n url: \"user/auth/sendPasswordResetCode\"\n });\n\n // User resets password after receiving code\n passwordReset = async(username: string, code: string, password: string): Promise<void> =>\n await this.call<void>({\n method: \"post\",\n params: {code, password, username},\n url: \"user/auth/passwordReset\"\n });\n\n // Confirm user signup after receiving code\n signUpConfirmation = async(username: string | null, code: string, email: string | null): Promise<void> => {\n const params = {...email && {email}, ...username && {username}, confirmationCode: code};\n\n return await this.call<void>({\n method: \"post\",\n params,\n url: \"user/auth/signupConfirmation\"\n });\n };\n\n // Lookup current user info\n getMe = async(): Promise<SelfUser> => {\n try {\n const userData = await this.call<SelfUser>({\n method: \"get\",\n url: \"user/auth/me\"\n });\n return userData;\n } catch (error) {\n throw new Error(\"Failed to get user info.\");\n }\n };\n\n // Get user info from the backend\n getUser = async(userId: string): Promise<User> =>\n await this.call<User>({\n method: \"get\",\n url: `user/${userId}`\n });\n\n // Get user info from the backend\n getUserMetadata = async(idToken: string): Promise<any[]> => {\n const response: any[] = await this.call({\n method: \"post\",\n params: {idToken},\n url: \"user/refreshMetadata\"\n });\n return response;\n };\n\n // Exchange Twitter token for PlayMakers tokens\n getTokensFromTwitterToken = async(accessToken: string, accessTokenSecret: string): Promise<unknown> =>\n await this.call({\n method: \"post\",\n params: {accessToken, accessTokenSecret},\n url: \"user/auth/exchangeTwitterToken\"\n });\n\n // Get last 5 creators\n getCreators = async(): Promise<User[]> =>\n await this.call<User[]>({\n method: \"get\",\n url: \"creators\"\n });\n\n updateAvatar = async(file: File): Promise<UserUpdateAvatarResponse> =>\n await this.call<UserUpdateAvatarResponse>({\n files: [file],\n method: \"put\",\n url: \"user\"\n });\n\n // Create a new parent account for an unlinked federated user, and link the two\n createUserParent = async(username: string, provider: string): Promise<UserCreateParentResponse> =>\n await this.call<UserCreateParentResponse>({\n method: \"post\",\n params: {provider, username},\n url: \"user/federated/createParent\"\n });\n\n // Link an existing account to an unlinked federated user, by access tokens\n linkFederated = async(accessToken: string, provider: string): Promise<UserLinkFederatedResponse> =>\n await this.call<UserLinkFederatedResponse>({\n method: \"post\",\n params: {accessToken, provider},\n url: \"user/federated/linkFederated\"\n });\n\n // Unlink a federated account, delete it\n unlinkFederated = async(sourceUsername: string, provider: string): Promise<SelfUser> =>\n await this.call<SelfUser>({\n method: \"post\",\n params: {provider, sourceUsername},\n url: \"user/federated/unlinkFederated\"\n });\n\n\n // ------------- VOTE -------------\n\n // Vote for a submission: upvote or downvote\n // TODO Deprecate ?\n vote = async(submissionId: string, direction: string): Promise<VotePostResponseType> =>\n await this.call<VotePostResponseType>({\n method: \"post\",\n url: `${direction === \"up\" ? \"vote/up\" : \"vote/down\"}/${submissionId}`\n });\n\n // Vote for a submission: downvote\n downVote = async(submissionId: string): Promise<VotePostResponseType> =>\n await this.call<VotePostResponseType>({\n method: \"post\",\n url: `vote/down/${submissionId}`\n });\n\n // Vote for a submission: upvote\n upVote = async(submissionId: string): Promise<VotePostResponseType> =>\n await this.call({\n method: \"post\",\n url: `vote/up/${submissionId}`\n });\n\n // Cancel one's vote\n cancelVote = async(submissionId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `vote/cancel/${submissionId}`\n });\n\n // Get current voting status\n getVote = async(submissionId: string): Promise<number> => {\n const vote = await this.call<VoteGetResponseType>({\n method: \"get\",\n url: `vote/${submissionId}`\n });\n return vote?.value || 0;\n };\n\n // Get voting balance for a given submission\n voteBalance = async(submissionId: string): Promise<number> => {\n const balance = await this.call<SubmissionVoteGetResponseType>({\n method: \"get\",\n url: `vote/balance/${submissionId}`\n });\n return balance?.balance || 0;\n };\n\n\n // ------------- NOTIFICATION -------------\n\n getNotifications = async(params: {[key: string]: unknown} = {}): Promise<NotificationType[]> =>\n await this.call<NotificationType[]>({\n method: \"get\",\n params,\n url: \"notifications\"\n });\n\n markAllNotificationsAsRead = async(): Promise<void> =>\n await this.call<void>({\n method: \"put\",\n url: \"notifications\"\n });\n\n updateNotification =\n async(notificationId: string, params: {[key: string]: unknown} = {}): Promise<NotificationType> =>\n await this.call<NotificationType>({\n method: \"put\",\n params,\n url: `notification/${notificationId}`\n });\n\n deleteNotification = async(notificationId: string): Promise<void> =>\n await this.call<void>({\n method: \"delete\",\n url: `notification/${notificationId}`\n });\n\n\n // ------------- TAG -------------\n\n getTags = async(projectId: string = this.projectId, params: {[key: string]: unknown} = {}): Promise<string[]> =>\n await this.call<string[]>({\n method: \"get\",\n params,\n url: `tags/by/project/${projectId}`\n });\n\n // ------------- TWITTER REDIRECTS -------------\n\n // Twitter endpoints for redirect\n // TODO replace with ID server\n\n redirectTwitterLogin = () => {\n window.location.replace(this.twitterSignInUrl);\n };\n\n // --------------- QUESTS TYPES ----------------\n // Quest types are the different types of quests available\n // They are used to create quests\n\n getSteamProfile = async() =>\n await this.call<unknown>({\n method: \"get\",\n url: \"quest/api/fetch/steam?operation=profile\"\n });\n\n getQuestType = async(questTypeId: string) =>\n await this.call<QuestType>({\n method: \"get\",\n url: `questTypes/${questTypeId}`\n });\n\n getQuestTypes = async(params: {[key: string]: any} = {}) =>\n await this.call<QuestType[]>({\n method: \"get\",\n params,\n url: \"questTypes\"\n });\n\n getQuestTypeCategories = async(params: {[key: string]: any} = {}) =>\n await this.call<string[]>({\n method: \"get\",\n params,\n url: \"questTypes/category\"\n });\n\n // --------------- QUESTS ----------------\n // All quests available\n\n getQuest = async(questId: string) =>\n await this.call<Quest>({\n method: \"get\",\n url: `quest/${questId}`\n });\n\n getQuests = async(params: {[key: string]: any}, projectId: string = this.projectId) =>\n await this.call<QuestWithQuestType[]>({\n method: \"get\",\n params,\n url: `quests/by/project/${projectId}`\n });\n\n getUserQuests = async(params: {[key: string]: any} = {}, projectId: string = this.projectId) =>\n await this.call<UserQuest[]>({\n method: \"get\",\n params,\n url: `userQuests/by/project/${projectId}`\n });\n\n completeQuest = async(questId: string, creatorData?: Record<string, unknown>) =>\n await this.call<UserQuest>({\n method: \"post\",\n params: creatorData,\n url: `quest/${questId}/complete`\n });\n\n createQuest = async(params: CreateQuest) =>\n await this.call<Quest>({\n method: \"post\",\n params,\n url: \"quest\"\n });\n\n updateQuest = async(questId: string, params: UpdateQuest) =>\n await this.call<Quest>({\n method: \"put\",\n params,\n url: `quest/${questId}`\n });\n\n deleteQuest = async(questId: string) =>\n await this.call({\n method: \"delete\",\n url: `quest/${questId}`\n });\n\n // --------------- USER QUESTS ----------------\n // Quests that the current user has completed\n\n getUserQuest = async(userQuestId: string) =>\n await this.call<UserQuest>({\n method: \"get\",\n url: `userQuest/${userQuestId}`\n });\n\n getUserQuestsByQuest = async(params: {[key: string]: any} = {}, questId: string) =>\n await this.call<UserQuest[]>({\n method: \"get\",\n params,\n url: `userQuests/by/quest/${questId}`\n });\n\n getUserQuestsByProject = async(params: {[key: string]: any} = {}, projectId: string = this.projectId) =>\n await this.call<UserQuest[]>({\n method: \"get\",\n params,\n url: `userQuests/by/project/${projectId}`\n });\n\n // --------------- REWARDS ----------------\n getRewardTypes = async(params: {[key: string]: any} = {}) =>\n await this.call<RewardType[]>({\n method: \"get\",\n params,\n url: \"rewardTypes\"\n });\n\n getRewardType = async(rewardTypeId: string) =>\n await this.call<RewardType>({\n method: \"get\",\n url: `rewardType/${rewardTypeId}`\n });\n\n getRewardsByProject = async(params: {[key: string]: any}, projectId: string = this.projectId) =>\n await this.call<Reward[]>({\n method: \"get\",\n params,\n url: `rewards/by/project/${projectId}`\n });\n\n getReward = async(rewardId: string) =>\n await this.call<Reward>({\n method: \"get\",\n url: `reward/${rewardId}`\n });\n\n redeemReward = async(rewardId: string) =>\n await this.call<UserReward>({\n method: \"post\",\n url: `reward/${rewardId}/redeem`\n });\n\n createReward = async(reward: CreateReward) =>\n await this.call<Reward>({\n method: \"post\",\n params: reward,\n url: \"reward\"\n });\n\n updateReward = async(rewardId: string, reward: UpdateReward) =>\n await this.call<Reward>({\n method: \"put\",\n params: reward,\n url: `reward/${rewardId}`\n });\n\n deleteReward = async(rewardId: string) =>\n await this.call({\n method: \"delete\",\n url: `reward/${rewardId}`\n });\n\n // --------------- USER REWARDS ----------------\n // Rewards for the current user\n\n getUserRewards = async(params: {[key: string]: any} = {}) =>\n await this.call<UserReward[]>({\n method: \"get\",\n params,\n url: \"userRewards\"\n });\n\n getUserRewardsByProject = async(params: {[key: string]: any} = {}, projectId: string = this.projectId) =>\n await this.call<UserReward[]>({\n method: \"get\",\n params,\n url: `userRewards/by/project/${projectId}`\n });\n\n getUserRewardsByReward = async(rewardId: string, params: {[key: string]: any} = {}) =>\n await this.call<UserReward[]>({\n method: \"get\",\n params,\n url: `userRewards/by/reward/${rewardId}`\n });\n\n getUserReward = async(userRewardId: string) =>\n await this.call<UserReward>({\n method: \"get\",\n url: `userReward/${userRewardId}`\n });\n\n // Update userReward state (pending --> redemeed, redeemed --> cancel)\n updateUserRewardState = async(userRewardId: string, state: string) =>\n await this.call<UserReward>({\n method: \"put\",\n params: {state},\n url: `userReward/${userRewardId}`\n });\n\n}\n\n\nexport {Api};\n","// @ts-nocheck\nimport React from \"react\";\nimport {CookiesProvider} from \"react-cookie\";\n\nimport {Api} from \"./Api\";\nimport {AuthProvider} from \"./AuthContext\";\nimport {PlayMakersConfig, PlayMakersProviderProps} from \"./types\";\n\nconst PlayMakersConfigDefault = {\n API_BASEURL: \"https://api.playmakers.co\",\n API_CACHE_EXPIRATION: 2000,\n BARISTA_BASEURL: \"https://barista.playmakers.co\",\n BARISTA_VERSION: \"latest\",\n COGNITO: {\n CLIENT_ID: \"1bkjbgkktdujdv23r62eue2elo\",\n DOMAIN: \"cognito.playmakers.co\",\n POOL_ID: \"eu-north-1_qMIFaOOnN\",\n REDIRECT_SIGNIN: [`${window.location.origin}/handle-from-federated`],\n REDIRECT_SIGNOUT: [`${window.location.origin}/handle-signout`],\n REGION: \"eu-north-1\"\n },\n IDSERVER_BASEURL: \"https://id.playmakers.co\",\n // How often in ms do we poll for notifications\n NOTIFICATIONS_POLL_PERIOD: 30000,\n PROJECT_ID: undefined,\n // How often in ms we poll the submission status for `pending...` to clear\n SUBMISSION_UPLOAD_POLL_PERIOD: 2000,\n // How long MH will wait in ms before displaying timeout message\n // Note if the submission takes longer it is not cancelled, it will still be processed\n SUBMISSION_UPLOAD_TIMEOUT: 90000,\n TWITTER: {\n CALLBACK_URL: `${window.location.origin}/handle-from-twitter`\n },\n UPLOADS_BASEURL: \"https://file.playmakers.co\"\n};\n\nconst PlayMakersContext = React.createContext<PlayMakersConfig>(PlayMakersConfigDefault);\nexport const usePlayMakersConfig = (): PlayMakersConfig => React.useContext(PlayMakersContext);\n\nexport const PlayMakersProvider = ({children, ...props}: PlayMakersProviderProps) => {\n const value = {...PlayMakersConfigDefault, ...props};\n value.COGNITO = {...PlayMakersConfigDefault.COGNITO, ...props.COGNITO};\n\n // TODO Might need to add a way to change PROJECT_ID on the fly, maybe ?\n\n return <PlayMakersContext.Provider value={value}>\n <CookiesProvider defaultSetOptions={{path: \"/\"}}>\n <ApiProvider>\n <AuthProvider>\n {children}\n </AuthProvider>\n </ApiProvider>\n </CookiesProvider>\n </PlayMakersContext.Provider>;\n};\n\n\nconst ApiContext = React.createContext<Api>({});\nexport const useApi = () => React.useContext(ApiContext);\n\nconst ApiProvider: React.FC<{children: React.ReactNode}> = ({children}) => {\n const config = usePlayMakersConfig();\n const api = new Api(config);\n\n const value = {};\n for(const method of api.exports) {\n value[method] = api[method];\n value[method].methodName = method;\n }\n\n return (\n <ApiContext.Provider value={value}>\n {children}\n </ApiContext.Provider>\n );\n};\n","export interface ProviderType {\n displayName: string;\n identityProvider: string | {custom: string};\n}\n\nexport interface ProvidersType {\n [key: string]: ProviderType;\n}\n\nexport const providers: ProvidersType = {\n discord: {\n displayName: \"Discord\",\n identityProvider: {custom: \"Discord\"}\n },\n facebook: {\n displayName: \"Facebook\",\n identityProvider: \"Facebook\"\n },\n google: {\n displayName: \"Google\",\n identityProvider: \"Google\"\n },\n twitch: {\n displayName: \"Twitch\",\n identityProvider: {custom: \"Twitch\"}\n },\n twitter: {\n displayName: \"X\",\n identityProvider: \"Twitter\"\n },\n ultra: {\n displayName: \"Ultra\",\n identityProvider: {custom: \"Ultra\"}\n }\n};\n\nexport type ProviderKeys = keyof typeof providers;\n","import {\n confirmSignIn as amplifyConfirmSignIn,\n ConfirmSignInOutput,\n fetchAuthSession,\n getCurrentUser,\n signIn as amplifySignIn,\n SignInOutput, signInWithRedirect as amplifySignInWithRedirect,\n signOut as amplifySignOut} from \"aws-amplify/auth\";\nimport {Hub} from \"aws-amplify/utils\";\nimport React, {createContext, useContext, useEffect, useState} from \"react\";\n\nimport {Api} from \"./Api\";\nimport {useApi} from \"./PlayMakersProvider\";\nimport {SelfUser, UserCreateParentResponse, UserUpdateAvatarResponse} from \"./types\";\nimport {providers, ProvidersType} from \"./utils/providers\";\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\ninterface AuthContextType {\n FederatedListener: React.FC;\n userData: SelfUser | null;\n providerData: ProvidersType;\n isLoggedIn: boolean;\n\n signOut: () => Promise<void>;\n signIn: (username: string, password: string) => Promise<SignInOutput>;\n signInWithRedirect: (provider: string) => Promise<void>;\n\n redirectTwitterLogin: () => void;\n linkFederated: Api[\"linkFederated\"];\n unlinkFederated: Api[\"unlinkFederated\"];\n getSteamProfile: Api[\"getSteamProfile\"];\n\n confirmSignIn: (newPassword: string) => Promise<ConfirmSignInOutput>;\n resendConfirmationCode: (email: string, username: string) => Promise<void>;\n\n signUp: Api[\"signUp\"];\n signUpConfirmation: Api[\"signUpConfirmation\"];\n\n sendForgotPassword: Api[\"sendPasswordResetCode\"];\n passwordReset: Api[\"passwordReset\"];\n\n updateUserAvatar: (avatar: File) => Promise<UserUpdateAvatarResponse>;\n createUserParent: (preferredUsername: string, provider: string) => Promise<UserCreateParentResponse>;\n refreshLoggedInUser: (existingUser?: SelfUser) => Promise<SelfUser | void>;\n getUserMetadata: () => Promise<any>;\n}\n\ninterface AuthProviderProps {\n children: React.ReactNode;\n}\nexport function AuthProvider({children}: AuthProviderProps) {\n\n const {\n signUp: signUpApi,\n signUpConfirmation: signUpConfirmationApi,\n getMe: getMeApi,\n resendConfirmationCode: resendConfirmationCodeApi,\n sendPasswordResetCode: sendPasswordResetCodeApi,\n passwordReset: passwordResetApi,\n updateAvatar: updateAvatarApi,\n createUserParent: createUserParentApi,\n linkFederated: linkFederatedApi,\n unlinkFederated: unlinkFederatedApi,\n redirectTwitterLogin: redirectTwitterLoginApi,\n getUserMetadata: getUserMetadataApi,\n getSteamProfile: getSteamProfileApi\n } = useApi();\n\n\n const [userData, setUserData] = useState<SelfUser | null>(null);\n\n const isLoggedIn = window.localStorage.getItem(\"amplify-authenticator-authState\") === \"signedIn\";\n\n interface AuthPayload {\n event: string;\n data?: unknown;\n }\n const FederatedListener: React.FC = () => {\n useEffect(() => {\n const listener = ({payload}: { payload: AuthPayload }) => {\n switch (payload.event) {\n case \"signInWithRedirect\":\n setLoggedInUser();\n break;\n case \"signInWithRedirect_failure\":\n console.error(\"An error has occurred during the OAuth flow.\");\n break;\n default:\n break;\n }\n };\n\n /* Add the listener. Call hubListenerCancelToken() to cancel the listener on unmount\n https://docs.amplify.aws/react/build-a-backend/auth/connect-your-frontend/\n listen-to-auth-events/#stop-listening-to-events\n */\n const hubListenerCancelToken = Hub.listen(\"auth\", listener);\n\n return () => {\n hubListenerCancelToken();\n };\n }, []);\n\n return null;\n };\n\n // Check inconsistency with with userData and loggedInUser, force logout if necessary\n // Can happen eg if amplify has cached data and changes are applied on the backend\n async function checkCognitoUser() {\n let cognitoUser;\n try {\n cognitoUser = await getCurrentUser();\n if(userData && !cognitoUser || !userData && cognitoUser ||\n userData?.username !== cognitoUser.username || userData.userId !== cognitoUser.userId) {\n if(cognitoUser) {\n // amplify and userData out of sync- force logout\n await signOut();\n }\n }\n } catch (e) {\n if(userData || cognitoUser) {\n // amplify and userData out of sync- force logout\n await signOut();\n }\n throw e;\n }\n }\n\n async function setLoggedInUser(existingUser?: SelfUser) {\n let me;\n try {\n if(existingUser)\n me = existingUser;\n else\n me = await getMeApi();\n if(!me)\n return;\n\n const session = await fetchAuthSession();\n\n const accessToken = session?.tokens?.accessToken?.toString();\n if(accessToken) me.accessToken = accessToken;\n\n const idToken = session?.tokens?.idToken?.toString();\n if(idToken)\n me.idToken = idToken;\n\n setUserData(me);\n window.localStorage.setItem(\"amplify-authenticator-authState\", \"signedIn\");\n return me;\n } catch (e) {\n console.error(\"error getting user\", e);\n setUserData(null);\n throw e;\n }\n }\n\n async function refreshLoggedInUser(existingUser?: SelfUser): Promise<SelfUser | void> {\n try {\n let user;\n if(existingUser)\n user = await setLoggedInUser(existingUser);\n else {\n // need to wait a little bit for cognito updates to propagate\n await new Promise(resolve => setTimeout(resolve, 1000));\n user = await setLoggedInUser();\n }\n if(!user)\n console.error(\"Failed to refresh local user\");\n return user;\n } catch (e) {\n console.error(\"Error refreshing local user\", e);\n throw e;\n }\n }\n\n const signIn = async(username: string, password: string) => {\n try {\n\n if(!username || !password) throw new Error(\"Missing username or password\");\n\n checkCognitoUser();\n const {isSignedIn, nextStep} = await amplifySignIn({password, username});\n\n if(nextStep.signInStep === \"DONE\" && isSignedIn)\n await setLoggedInUser();\n\n return {isSignedIn, nextStep};\n } catch (e) {\n console.error(\"Error signing in\", e);\n throw e;\n }\n };\n\n async function getUserMetadata() {\n try {\n if(!userData?.idToken)\n return;\n return await getUserMetadataApi(userData.idToken);\n } catch (e) {\n console.error(\"Error getting metadata\", e);\n throw e;\n }\n }\n\n\n async function confirmSignIn(newPassword: string) {\n const {isSignedIn, nextStep} = await amplifyConfirmSignIn({challengeResponse: newPassword});\n if(nextStep.signInStep === \"DONE\" && isSignedIn) setLoggedInUser();\n return {isSignedIn, nextStep};\n }\n\n async function passwordReset(username: string, code: string, newPassword: string) {\n signOut();\n return await passwordResetApi(username, code, newPassword);\n }\n\n async function signOut() {\n await amplifySignOut();\n\n window.localStorage.removeItem(\"amplify-authenticator-authState\");\n setUserData(null);\n }\n\n async function updateUserAvatar(avatar: File) {\n const response = await updateAvatarApi(avatar);\n if(response?.url) {\n setUserData(user => {\n if(user)\n user.avatar = response.url;\n return user;\n });\n }\n return response;\n }\n\n async function signInWithRedirect(provider: string) {\n checkCognitoUser();\n const providerName = providers[provider].identityProvider;\n\n // @ts-expect-error - AuthProvider does not include our custom providers ex: \"Twitter\"\n await amplifySignInWithRedirect({provider: providerName});\n }\n\n async function handleOnLoad() {\n setLoggedInUser();\n }\n\n const getSteamProfile = async(): Promise<unknown> => {\n try {\n const response = await getSteamProfileApi();\n if(response)\n await refreshLoggedInUser();\n return response;\n } catch (error) {\n console.error(\"Error fetching steam profile\", error);\n throw new Error(\"Error fetching steam profile\", {cause: error});\n }\n };\n\n useEffect(() => {\n handleOnLoad();\n }, []);\n\n\n const auth: AuthContextType = {\n FederatedListener,\n confirmSignIn,\n createUserParent: createUserParentApi,\n getSteamProfile,\n getUserMetadata,\n isLoggedIn,\n linkFederated: linkFederatedApi,\n passwordReset,\n providerData: providers,\n redirectTwitterLogin: redirectTwitterLoginApi,\n refreshLoggedInUser,\n resendConfirmationCode: resendConfirmationCodeApi,\n sendForgotPassword: sendPasswordResetCodeApi,\n signIn,\n signInWithRedirect,\n signOut,\n signUp: signUpApi,\n signUpConfirmation: signUpConfirmationApi,\n unlinkFederated: unlinkFederatedApi,\n updateUserAvatar,\n userData\n };\n\n return (\n <AuthContext.Provider value={auth}>\n {children}\n </AuthContext.Provider>\n );\n}\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if(!context)\n throw new Error(\"useAuth must be used within an AuthProvider\");\n return context;\n}\n","const cache: {[key: string]: {\n response: unknown,\n timestamp: number\n}} = {};\n\nexport const getCache = async<T>(key: string, cacheExpiration: number = 2000): Promise<T | undefined> => {\n const cacheData = cache[key];\n if(cacheData && (new Date).getTime() - cacheData.timestamp < cacheExpiration)\n return await cacheData.response as T;\n return undefined;\n};\n\nexport const setCache = (key: string, response: unknown) => {\n cache[key] = {\n response,\n timestamp: (new Date).getTime()\n };\n};\n\nexport const withCache =\nasync<T, S extends unknown[]>(fun: (...args: S) => Promise<T>, args: S, cacheExpiration: number): Promise<T> => {\n\n // @ts-expect-error - This is a hack to get the function name\n const key = `${fun.name||fun.methodName}(${JSON.stringify(args)})`;\n\n const data = await getCache<T>(key, cacheExpiration);\n if(data !== undefined) return data;\n const cacheData = fun(...args);\n setCache(key, cacheData);\n return await cacheData;\n};\n\nexport const clearCache = (functionKey: string, args?: unknown[]) => {\n // If fun is not defined, we clear the whole cache\n if(!functionKey) {\n for(const key in cache) delete cache[key];\n return;\n }\n // If there are args, we clear the one entry in the cache\n if(args) {\n delete cache[`${functionKey}(${JSON.stringify(args)})`];\n return;\n }\n // If there is a functionKey, but no args, we clear all the entries related to the fun\n for(const k in cache) {\n if(k.startsWith(`${functionKey}(`))\n delete cache[k];\n }\n\n};\n","import React, {Dispatch, useState} from \"react\";\n\nexport const usePagination = <T>(\n setData: Dispatch<React.SetStateAction<T[]>>,\n setHasMore: Dispatch<React.SetStateAction<boolean>>,\n hasMore: boolean = true,\n initialPage: number = 1,\n limit: number = 10\n) => {\n const [page, setPage] = useState<number>(initialPage);\n\n const appendNextPage = async(\n fetch: ({page, limit}: {page: number; limit: number}) => Promise<T[]>,\n mutation?: (data: T) => T\n ) => {\n if(!hasMore) return;\n\n const newPage = page + 1;\n const newData = await fetch({limit, page: newPage});\n const mutatedData = mutation ? newData.map(mutation) : newData;\n setData(prevProjects => {\n prevProjects.push(...mutatedData);\n return prevProjects;\n });\n\n checkHasMore(mutatedData);\n\n setPage(newPage);\n };\n\n const refreshLastPage = async(\n fetch: ({page, limit}: {page: number; limit: number}) => Promise<T[]>,\n mutation?: (data: T) => T\n ) => {\n const newData = await fetch({limit, page});\n const mutatedData = mutation ? newData.map(mutation) : newData;\n setData(prevProjects => {\n const itemsToRemove = prevProjects.length % limit;\n prevProjects.splice(-itemsToRemove, itemsToRemove);\n prevProjects.push(...mutatedData);\n return prevProjects;\n });\n\n checkHasMore(mutatedData);\n };\n\n const checkHasMore = <D>(newData: D[]): D[] => {\n if(newData.length < (limit || 10)) setHasMore(false);\n return newData;\n };\n\n return {\n appendNextPage,\n checkHasMore,\n currentPage: page,\n hasMore,\n refreshLastPage\n };\n};\n","import React, {useEffect, useState} from \"react\";\n\nimport {clearCache, withCache} from \"../cache\";\nimport {useApi} from \"../PlayMakersProvider\";\nimport {AssetExtendedType, AssetPayloadType, AssetType} from \"../types/assets.types\";\nimport {usePagination} from \"./usePagination\";\n\ntype AssetPushMethod = (updatedAsset?: Partial<AssetPayloadType & {newFile: File}>) => Promise<AssetType>;\ntype AssetFetchMethod = (quiet?: boolean) => Promise<AssetType>;\ntype AssetPushFileMethod = (file: File | null) => Promise<void>;\ntype AssetDeleteMethod = () => Promise<void>;\n\ninterface UseAssetReturn extends Partial<AssetType> {\n asset: AssetType | null;\n fetched: boolean;\n setAsset: React.Dispatch<React.SetStateAction<AssetType | null>>;\n setFile: React.Dispatch<React.SetStateAction<File | null>>;\n push: AssetPushMethod;\n delete: AssetDeleteMethod;\n pushFile: AssetPushFileMethod;\n refresh: AssetFetchMethod;\n}\n\nexport const useAsset = (id?: string): UseAssetReturn => {\n const {createAsset, getAsset, updateAsset, updateAssetFile, deleteAsset} = useApi();\n\n let assetId = id;\n\n const [asset, setAsset] = useState<AssetType | null>(null);\n const [fetched, setFetched] = useState<boolean>(false);\n const [file, setFile] = useState<File | null>(null);\n\n const fetchAsset: AssetFetchMethod = async(quiet = false) => {\n try {\n if(!id) {\n setAsset(null);\n setFetched(false);\n throw new Error(\"N