diff --git a/package.json b/package.json index dafc64d210..cc31c1b5e2 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@jest/globals": "^29.7.0", "@tsconfig/docusaurus": "^2.0.3", "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.14", "@typescript-eslint/eslint-plugin": "^8.11.0", "concurrently": "^8.2.1", "eslint": "^8.0.1", diff --git a/src/cadence/contracts/GoldStar.cdc b/src/cadence/contracts/GoldStar.cdc index ed81c861b5..fb2c1bd34c 100644 --- a/src/cadence/contracts/GoldStar.cdc +++ b/src/cadence/contracts/GoldStar.cdc @@ -27,7 +27,7 @@ contract GoldStar { access(all) resource Profile { - access(all) + access(mapping Identity) var handle: ProfileHandle access(all) diff --git a/src/cadence/transactions/GoldStar/UpdateProfile.cdc b/src/cadence/transactions/GoldStar/UpdateProfile.cdc index 879313c5ea..cec6634861 100644 --- a/src/cadence/transactions/GoldStar/UpdateProfile.cdc +++ b/src/cadence/transactions/GoldStar/UpdateProfile.cdc @@ -7,15 +7,19 @@ transaction( deployedContracts: {Address: [String]}, socials: {String: String} ) { - let profile: auth(GoldStar.UpdateSocials, GoldStar.UpdateDeployedContracts, GoldStar.UpdateReferralSource) &GoldStar.Profile + let profile: auth(GoldStar.UpdateHandle, GoldStar.UpdateSocials, GoldStar.UpdateDeployedContracts, GoldStar.UpdateReferralSource) &GoldStar.Profile prepare(signer: auth(BorrowValue) &Account) { self.profile = signer.storage - .borrow(from: GoldStar.profileStoragePath) + .borrow(from: GoldStar.profileStoragePath) ?? panic("missing profile") } execute { + // Update the handle + self.profile.handle.update(newHandle: handle) + + // Update the referral source if let referralSource = referralSource { self.profile.updateReferralSource(source: referralSource) } diff --git a/src/components/ConnectButton.tsx b/src/components/ConnectButton.tsx index 92763569c6..2037ac83d4 100644 --- a/src/components/ConnectButton.tsx +++ b/src/components/ConnectButton.tsx @@ -53,10 +53,20 @@ const ConnectButton: React.FC = () => { return ( <> - - + { + handleCloseProgressModal(); + handleOpenProfileModal(); + }} + /> + - ) + ); }; export default ConnectButton; diff --git a/src/components/ProfileModal.tsx b/src/components/ProfileModal.tsx index eddcfe17fe..ac8b4e2ebc 100644 --- a/src/components/ProfileModal.tsx +++ b/src/components/ProfileModal.tsx @@ -1,9 +1,14 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Input from '@site/src/ui/design-system/src/lib/Components/Input'; import Field from '@site/src/ui/design-system/src/lib/Components/Field'; import Modal from '@site/src/ui/design-system/src/lib/Components/Modal'; import RadioGroup from '@site/src/ui/design-system/src/lib/Components/RadioGroup'; import { Button } from '@site/src/ui/design-system/src/lib/Components/Button'; +import { ProfileSettings, SocialType } from '../types/gold-star'; +import { useProfile } from '../hooks/use-profile'; +import { useCurrentUser } from '../hooks/use-current-user'; +import { createProfile, setProfile } from '../utils/gold-star'; +import { isEqual } from 'lodash'; import RemovableTag from '@site/src/ui/design-system/src/lib/Components/RemovableTag'; interface ProfileModalProps { @@ -21,10 +26,36 @@ const flowSources = [ ]; const ProfileModal: React.FC = ({ isOpen, onClose }) => { - const [selectedSource, setSelectedSource] = useState(flowSources[0].name); + const { user } = useCurrentUser(); + const { + profile, + isLoading, + error, + mutate: mutateProfile, + } = useProfile(user.addr); + const [settings, setSettings] = useState({ + handle: '', + socials: {}, + referralSource: '', + deployedContracts: {}, + }); + const [loaded, setLoaded] = useState(false); + const [isSaving, setIsSaving] = useState(false); const [tags, setTags] = useState([]); const [tagInput, setTagInput] = useState(''); + useEffect(() => { + if (profile && !loaded && !isLoading && !error) { + setSettings({ + handle: profile.handle, + socials: profile.socials, + referralSource: profile.referralSource, + deployedContracts: profile.deployedContracts, + }); + setLoaded(true); + } + }, [profile, settings, loaded, isLoading, error]); + const handleAddTag = () => { if (tagInput.trim() && !tags.includes(tagInput.trim())) { setTags([...tags, tagInput.trim()]); @@ -36,19 +67,66 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { setTags(tags.filter((tag) => tag !== tagToRemove)); }; + async function handleSave() { + if (!settings) return; + + setIsSaving(true); + try { + if (profile) { + await setProfile(settings); + } else { + await createProfile(settings); + } + } catch (e) { + console.error(e); + } finally { + setIsSaving(false); + mutateProfile(); + } + } + + function hasChanges() { + return ( + !isEqual(profile?.handle, settings?.handle) || + !isEqual(profile?.socials, settings?.socials) || + !isEqual(profile?.referralSource, settings?.referralSource) || + !isEqual(profile?.deployedContracts, settings?.deployedContracts) + ); + } + return (
- + + setSettings({ ...settings, handle: e.target.value }) + } + /> - + + setSettings({ + ...settings, + socials: { [SocialType.GITHUB]: e.target.value }, + }) + } + />
- +
= ({ isOpen, onClose }) => {
source.name)} - value={selectedSource} - onChange={setSelectedSource} + value={settings?.referralSource || ''} + onChange={(value) => + setSettings({ ...settings, referralSource: value }) + } label="How did you find Flow?" getDescription={(option) => - flowSources.find((source) => source.name === option)?.description || '' + flowSources.find((source) => source.name === option) + ?.description || '' } />
-
-
diff --git a/src/components/ProgressModal.tsx b/src/components/ProgressModal.tsx index 53e443f22f..1d8ec75128 100644 --- a/src/components/ProgressModal.tsx +++ b/src/components/ProgressModal.tsx @@ -9,9 +9,14 @@ import { SocialType } from '../types/gold-star'; interface ProgressModalProps { isOpen: boolean; onClose: () => void; + onOpenProfileModal: () => void; } -const ProgressModal: React.FC = ({ isOpen, onClose }) => { +const ProgressModal: React.FC = ({ + isOpen, + onClose, + onOpenProfileModal, +}) => { const user = useCurrentUser(); const { profile } = useProfile(user.user.addr); @@ -38,10 +43,6 @@ const ProgressModal: React.FC = ({ isOpen, onClose }) => { { label: 'Complete first challenge', completed: true }, ]; - const onProfileAction = () => { - console.log('TODO: Profile action'); - }; - const onChallengeAction = () => { console.log('TODO: Challenge action'); }; @@ -56,7 +57,7 @@ const ProgressModal: React.FC = ({ isOpen, onClose }) => { diff --git a/src/utils/gold-star.ts b/src/utils/gold-star.ts index 658916aa03..b04a7e2576 100644 --- a/src/utils/gold-star.ts +++ b/src/utils/gold-star.ts @@ -57,9 +57,22 @@ export const createProfile = async (profile: ProfileSettings) => { args: (arg, t) => [ arg(profile.handle, t.String), arg(profile.referralSource, t.Optional(t.String)), - arg(profile.socials, t.Dictionary(t.String, t.String)), arg( - profile.deployedContracts, + profile.socials + ? Object.entries(profile.socials).map(([key, value]) => ({ + key, + value, + })) + : [], + t.Dictionary(t.Address, t.String), + ), + arg( + profile.deployedContracts + ? Object.entries(profile.deployedContracts).map(([key, value]) => ({ + key, + value, + })) + : [], t.Dictionary(t.Address, t.Array(t.String)), ), ], @@ -77,9 +90,22 @@ export const setProfile = async (profile: ProfileSettings) => { args: (arg, t) => [ arg(profile.handle, t.String), arg(profile.referralSource, t.Optional(t.String)), - arg(profile.socials, t.Dictionary(t.String, t.String)), arg( - profile.deployedContracts, + profile.socials + ? Object.entries(profile.socials).map(([key, value]) => ({ + key, + value, + })) + : [], + t.Dictionary(t.Address, t.String), + ), + arg( + profile.deployedContracts + ? Object.entries(profile.deployedContracts).map(([key, value]) => ({ + key, + value, + })) + : [], t.Dictionary(t.Address, t.Array(t.String)), ), ], diff --git a/yarn.lock b/yarn.lock index ad25e3c73d..744a88f0bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5113,6 +5113,11 @@ resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5" integrity sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg== +"@types/lodash@^4.17.14": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.14.tgz#bafc053533f4cdc5fcc9635af46a963c1f3deaff" + integrity sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A== + "@types/mdast@^3.0.0": version "3.0.15" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" @@ -17086,11 +17091,6 @@ use-sync-external-store@^1.2.0, use-sync-external-store@^1.4.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== -use-sync-external-store@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" - integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"