diff --git a/package.json b/package.json index cc31c1b5e2..0a1d795bd5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^3.6.3", "@jest/globals": "^29.7.0", + "@onflow/typedefs": "^1.4.0", "@tsconfig/docusaurus": "^2.0.3", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.14", diff --git a/src/components/ChallengeModal.tsx b/src/components/ChallengeModal.tsx new file mode 100644 index 0000000000..f1b0d9ef2f --- /dev/null +++ b/src/components/ChallengeModal.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import { Button } from '../ui/design-system/src/lib/Components/Button'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBrain } from '@fortawesome/free-solid-svg-icons'; +import Modal from '@site/src/ui/design-system/src/lib/Components/Modal'; +import { submitNoopChallenge } from '../utils/gold-star'; +import * as fcl from '@onflow/fcl'; +import { useProfile } from '../hooks/use-profile'; +import { useCurrentUser } from '../hooks/use-current-user'; +import { getChallengeIdentifier } from '../utils/flow'; +import { GOLD_STAR_CHALLENGES } from '../utils/constants'; + +const ChallengeModal: React.FC<{ + isOpen: boolean; + onClose: () => void; +}> = ({ isOpen, onClose }) => { + const [txStatus, setTxStatus] = useState(null); + const { user } = useCurrentUser(); + const { profile, mutate: mutateProfile } = useProfile(user.addr); + + const completeChallenge = async () => { + setTxStatus('Pending Approval...'); + try { + const txId = await submitNoopChallenge(); + + setTxStatus('Submitting Challenge...'); + + fcl + .tx(txId) + .onceSealed() + .then(() => { + mutateProfile(); + }); + + await fcl.tx(txId).onceExecuted(); + + if (profile) { + mutateProfile( + { + ...profile, + submissions: { + [getChallengeIdentifier(GOLD_STAR_CHALLENGES.NOOP_CHALLENGE)]: { + completed: true, + }, + }, + }, + false, + ); + } + } catch (error) { + console.error('Failed to complete challenge', error); + } finally { + setTxStatus(null); + onClose(); + } + }; + + return ( + +
+ +

+ By completing this challenge, you are declaring your intent to learn + Flow and explore its capabilities. +

+ +
+
+ ); +}; + +export default ChallengeModal; diff --git a/src/components/ProfileModal.tsx b/src/components/ProfileModal.tsx index e77af24aa1..8838dd43b5 100644 --- a/src/components/ProfileModal.tsx +++ b/src/components/ProfileModal.tsx @@ -13,6 +13,7 @@ import RemovableTag from '@site/src/ui/design-system/src/lib/Components/Removabl import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrophy } from '@fortawesome/free-solid-svg-icons'; import { useChallenges } from '../hooks/use-challenges'; +import * as fcl from '@onflow/fcl'; interface ProfileModalProps { isOpen: boolean; @@ -44,7 +45,7 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { deployedContracts: {}, }); const [loaded, setLoaded] = useState(false); - const [isSaving, setIsSaving] = useState(false); + const [txStatus, setTxStatus] = useState(null); const [tags, setTags] = useState([]); const [tagInput, setTagInput] = useState(''); @@ -85,18 +86,41 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { async function handleSave() { if (!settings) return; - setIsSaving(true); + setTxStatus('Pending Approval...'); try { + let txId: string | null = null; if (profile) { - await setProfile(settings); + txId = await setProfile(settings); } else { - await createProfile(settings); + txId = await createProfile(settings); + } + + setTxStatus('Saving Profile...'); + + fcl + .tx(txId) + .onceSealed() + .then(() => { + mutateProfile(); + }); + + await fcl.tx(txId).onceExecuted(); + + if (profile) { + mutateProfile({ + ...profile, + handle: settings.handle || profile.handle, + socials: settings.socials || profile.socials, + referralSource: settings.referralSource || profile.referralSource, + deployedContracts: + settings.deployedContracts || profile.deployedContracts, + }); } } catch (e) { console.error(e); } finally { - setIsSaving(false); - mutateProfile(); + setTxStatus(null); + onClose(); } } @@ -211,9 +235,9 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { size="sm" className="w-full max-w-md" onClick={handleSave} - disabled={!hasChanges() || !settings || isSaving} + disabled={!hasChanges() || !settings || !!txStatus} > - Save + {txStatus ? txStatus : 'Save Profile'} diff --git a/src/components/ProgressModal.tsx b/src/components/ProgressModal.tsx index e62d0ffef1..7baf6cdb80 100644 --- a/src/components/ProgressModal.tsx +++ b/src/components/ProgressModal.tsx @@ -3,9 +3,8 @@ import Modal from '@site/src/ui/design-system/src/lib/Components/Modal'; import Checklist from '@site/src/components/ProgressChecklist'; import { Button } from '@site/src/ui/design-system/src/lib/Components/Button'; import { useProgress } from '../hooks/use-progress'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faBrain } from '@fortawesome/free-solid-svg-icons'; import { submitNoopChallenge } from '../utils/gold-star'; +import ChallengeModal from './ChallengeModal'; interface ProgressModalProps { isOpen: boolean; @@ -30,11 +29,6 @@ const ProgressModal: React.FC = ({ setChallengeModalOpen(false); }; - const completeChallenge = async () => { - await submitNoopChallenge(); - closeChallengeModal(); - }; - return ( <> @@ -69,33 +63,10 @@ const ProgressModal: React.FC = ({ - {/* Challenge Modal */} - {challengeModalOpen && ( - -
- -

- By completing this challenge, you are declaring your intent to - learn Flow and explore its capabilities. -

- -
-
- )} + ); }; diff --git a/yarn.lock b/yarn.lock index 744a88f0bd..a0c5689016 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3387,7 +3387,7 @@ isomorphic-ws "^5.0.0" ws "^8.18.0" -"@onflow/typedefs@1.4.0": +"@onflow/typedefs@1.4.0", "@onflow/typedefs@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@onflow/typedefs/-/typedefs-1.4.0.tgz#21963c4a334cb15f7759b90f25eee177d32885f6" integrity sha512-7b4C3F4Ztayx6XdQz/7YoHMzZ6kzy37dLxdVCV/PAsAunq9Jfu32HQaf8a0NCk0L0aM7FS2zT1Om4k7b5KP4Xg==