diff --git a/cypress/e2e/gno/organization-creation.cy.ts b/cypress/e2e/gno/organizations/membership-organization-creation.cy.ts similarity index 91% rename from cypress/e2e/gno/organization-creation.cy.ts rename to cypress/e2e/gno/organizations/membership-organization-creation.cy.ts index 81bc06c404..92899ce177 100644 --- a/cypress/e2e/gno/organization-creation.cy.ts +++ b/cypress/e2e/gno/organizations/membership-organization-creation.cy.ts @@ -1,4 +1,4 @@ -import { connectWallet, resetChain } from "./lib"; +import { connectWallet, resetChain } from "../lib"; describe("Create an organization flow", () => { it("works", () => { @@ -26,7 +26,7 @@ describe("Create an organization flow", () => { cy.get('[data-testid="organization-description"]').type(description); cy.contains("Next: Configure voting").click(); - cy.contains("Next: Set tokens or members").click(); + cy.contains("Next: Set members").click(); cy.get('[data-testid="member-settings-next"]').click(); cy.contains("Confirm & Launch the Organization").click(); diff --git a/cypress/e2e/gno/projects-contractor.cy.ts b/cypress/e2e/gno/projects/projects-contractor.cy.ts similarity index 99% rename from cypress/e2e/gno/projects-contractor.cy.ts rename to cypress/e2e/gno/projects/projects-contractor.cy.ts index 564ac9ccc6..d2f0a84c5c 100644 --- a/cypress/e2e/gno/projects-contractor.cy.ts +++ b/cypress/e2e/gno/projects/projects-contractor.cy.ts @@ -2,7 +2,7 @@ import { changeSelectedMilestoneStatus, changeTestUser, connectWallet, -} from "./lib"; +} from "../lib"; describe("Contractor proposer full flow", () => { it("works", () => { diff --git a/cypress/e2e/gno/projects-funder.cy.ts b/cypress/e2e/gno/projects/projects-funder.cy.ts similarity index 99% rename from cypress/e2e/gno/projects-funder.cy.ts rename to cypress/e2e/gno/projects/projects-funder.cy.ts index 6fcde6f7fc..a62185bb14 100644 --- a/cypress/e2e/gno/projects-funder.cy.ts +++ b/cypress/e2e/gno/projects/projects-funder.cy.ts @@ -2,7 +2,7 @@ import { changeSelectedMilestoneStatus, changeTestUser, connectWallet, -} from "./lib"; +} from "../lib"; describe("Funder proposer full flow", () => { it("works", () => { diff --git a/cypress/e2e/gno/upp-form.cy.ts b/cypress/e2e/gno/upp/upp-form.cy.ts similarity index 96% rename from cypress/e2e/gno/upp-form.cy.ts rename to cypress/e2e/gno/upp/upp-form.cy.ts index 84d096c785..1b50b5dde1 100644 --- a/cypress/e2e/gno/upp-form.cy.ts +++ b/cypress/e2e/gno/upp/upp-form.cy.ts @@ -1,4 +1,4 @@ -import { changeTestUser, connectWallet, resetChain } from "./lib"; +import { changeTestUser, connectWallet, resetChain } from "../lib"; const showUppForm = () => { cy.contains("Edit profile").click(); diff --git a/gno/p/dao_core/dao_core.gno b/gno/p/dao_core/dao_core.gno index fc9f4af627..2f83fca06f 100644 --- a/gno/p/dao_core/dao_core.gno +++ b/gno/p/dao_core/dao_core.gno @@ -5,7 +5,9 @@ import ( "strconv" "strings" + "gno.land/p/demo/json" dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/jsonutil" ) // TODO: add wrapper message handler to handle multiple proposal modules messages @@ -14,6 +16,7 @@ type daoCore struct { dao_interfaces.IDAOCore votingModule dao_interfaces.IVotingModule + rolesModule dao_interfaces.IRolesModule proposalModules []dao_interfaces.ActivableProposalModule activeProposalModuleCount int realm std.Realm @@ -22,6 +25,7 @@ type daoCore struct { func NewDAOCore( votingModuleFactory dao_interfaces.VotingModuleFactory, + rolesModuleFactory dao_interfaces.RolesModuleFactory, proposalModulesFactories []dao_interfaces.ProposalModuleFactory, messageHandlersFactories []dao_interfaces.MessageHandlerFactory, ) dao_interfaces.IDAOCore { @@ -29,6 +33,10 @@ func NewDAOCore( panic("Missing voting module factory") } + if rolesModuleFactory == nil { + panic("Missing roles module factory") + } + if len(proposalModulesFactories) == 0 { panic("No proposal modules factories") } @@ -45,6 +53,11 @@ func NewDAOCore( panic("voting module factory returned nil") } + core.rolesModule = rolesModuleFactory(core) + if core.rolesModule == nil { + panic("roles module factory returned nil") + } + for i, modFactory := range proposalModulesFactories { mod := modFactory(core) if mod == nil { @@ -117,6 +130,32 @@ func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { return d.votingModule } +func (d *daoCore) RolesModule() dao_interfaces.IRolesModule { + return d.rolesModule +} + +func (d *daoCore) GetMembersJSON(start, end string, limit uint64, height int64) string { + vMembers := d.votingModule.GetMembersJSON(start, end, limit, height) + nodes, err := json.Unmarshal([]byte(vMembers)) + if err != nil { + panic("failed to unmarshal voting module members") + } + vals := nodes.MustArray() + for i, val := range vals { + obj := val.MustObject() + addr := jsonutil.MustAddress(obj["address"]) + roles := d.rolesModule.GetMemberRoles(addr) + rolesJSON := make([]*json.Node, len(roles)) + for j, role := range roles { + rolesJSON[j] = json.StringNode("", role) + } + obj["roles"] = json.ArrayNode("", rolesJSON) + vals[i] = json.ObjectNode("", obj) + + } + return json.ArrayNode("", vals).String() +} + func (d *daoCore) VotingPowerAtHeight(address std.Address, height int64) uint64 { return d.VotingModule().VotingPowerAtHeight(address, height) } @@ -133,6 +172,11 @@ func (d *daoCore) Render(path string) string { sb.WriteString(votingInfo.String()) sb.WriteRune('\n') sb.WriteString(d.votingModule.Render("")) + rolesInfo := d.rolesModule.Info() + sb.WriteString("# Roles Module: ") + sb.WriteString(rolesInfo.String()) + sb.WriteRune('\n') + sb.WriteString(d.rolesModule.Render("")) sb.WriteString("## Supported Messages:\n") sb.WriteString(d.registry.Render()) diff --git a/gno/p/dao_core/dao_core_test.gno b/gno/p/dao_core/dao_core_test.gno index 79ff3fab98..4a879aacdd 100644 --- a/gno/p/dao_core/dao_core_test.gno +++ b/gno/p/dao_core/dao_core_test.gno @@ -46,6 +46,53 @@ func (vm *votingModule) TotalPowerAtHeight(height int64) uint64 { return 0 } +type rolesModule struct { + core dao_interfaces.IDAOCore +} + +func rolesModuleFactory(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + return &rolesModule{core: core} +} + +func (rm *rolesModule) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "TestRoles", + Version: "42.21", + } +} + +func (rm *rolesModule) ConfigJSON() string { + return "{}" +} + +func (rm *rolesModule) Render(path string) string { + return "# Test Roles Module" +} + +func (rm *rolesModule) HasRole(address std.Address, role string) bool { + return false +} + +func (rm *rolesModule) NewRole(roleName string) { + panic("not implemented") +} + +func (rm *rolesModule) DeleteRole(roleName string) { + panic("not implemented") +} + +func (rm *rolesModule) GrantRole(address std.Address, role string) { + panic("not implemented") +} + +func (rm *rolesModule) RevokeRole(address std.Address, role string) { + panic("not implemented") +} + +func (rm *rolesModule) GetMemberRoles(address std.Address) []string { + return []string{} +} + type proposalModule struct { core dao_interfaces.IDAOCore } @@ -100,7 +147,7 @@ func TestDAOCore(t *testing.T) { return handler } - core := NewDAOCore(votingModuleFactory, []dao_interfaces.ProposalModuleFactory{proposalModuleFactory}, []dao_interfaces.MessageHandlerFactory{handlerFactory}) + core := NewDAOCore(votingModuleFactory, rolesModuleFactory, []dao_interfaces.ProposalModuleFactory{proposalModuleFactory}, []dao_interfaces.MessageHandlerFactory{handlerFactory}) if core == nil { t.Fatal("core is nil") } @@ -118,6 +165,15 @@ func TestDAOCore(t *testing.T) { t.Fatal("voting module has wrong kind") } + rolesMod := core.RolesModule() + if rolesMod == nil { + t.Fatal("roles module is nil") + } + + if rolesMod.Info().Kind != "TestRoles" { + t.Fatal("roles module has wrong kind") + } + propMods := core.ProposalModules() if len(propMods) != 1 { t.Fatal("expected 1 proposal module") diff --git a/gno/p/dao_core/gno.mod b/gno/p/dao_core/gno.mod index f19b7aa42d..7dd48bc44d 100644 --- a/gno/p/dao_core/gno.mod +++ b/gno/p/dao_core/gno.mod @@ -3,4 +3,5 @@ module gno.land/p/teritori/dao_core require ( gno.land/p/demo/json v0.0.0-latest gno.land/p/teritori/dao_interfaces v0.0.0-latest + gno.land/p/teritori/jsonutil v0.0.0-latest ) diff --git a/gno/p/dao_interfaces/core.gno b/gno/p/dao_interfaces/core.gno index 5379638edb..749a0ccf69 100644 --- a/gno/p/dao_interfaces/core.gno +++ b/gno/p/dao_interfaces/core.gno @@ -11,10 +11,13 @@ type IDAOCore interface { Render(path string) string VotingModule() IVotingModule + RolesModule() IRolesModule ProposalModules() []ActivableProposalModule ActiveProposalModuleCount() int Registry() *MessagesRegistry UpdateVotingModule(newVotingModule IVotingModule) UpdateProposalModules(toAdd []IProposalModule, toDisable []int) + + GetMembersJSON(start, end string, limit uint64, height int64) string } diff --git a/gno/p/dao_interfaces/core_testing.gno b/gno/p/dao_interfaces/core_testing.gno index 76e1cec0c7..a61c467bdf 100644 --- a/gno/p/dao_interfaces/core_testing.gno +++ b/gno/p/dao_interfaces/core_testing.gno @@ -14,6 +14,10 @@ func (d *dummyCore) VotingModule() IVotingModule { panic("not implemented") } +func (d *dummyCore) RolesModule() IRolesModule { + panic("not implemented") +} + func (d *dummyCore) ProposalModules() []ActivableProposalModule { panic("not implemented") } @@ -33,3 +37,7 @@ func (d *dummyCore) UpdateVotingModule(newVotingModule IVotingModule) { func (d *dummyCore) UpdateProposalModules(toAdd []IProposalModule, toDisable []int) { panic("not implemented") } + +func (d *dummyCore) GetMembersJSON(start, end string, limit uint64, height int64) string { + panic("not implemented") +} diff --git a/gno/p/dao_interfaces/modules.gno b/gno/p/dao_interfaces/modules.gno index 3219580a16..d5b4d7c3a5 100644 --- a/gno/p/dao_interfaces/modules.gno +++ b/gno/p/dao_interfaces/modules.gno @@ -37,3 +37,17 @@ type IProposalModule interface { } type ProposalModuleFactory func(core IDAOCore) IProposalModule + +type IRolesModule interface { + Info() ModuleInfo + ConfigJSON() string + Render(path string) string + GetMemberRoles(address std.Address) []string + HasRole(address std.Address, role string) bool + NewRole(roleName string) + DeleteRole(roleName string) + GrantRole(address std.Address, role string) + RevokeRole(address std.Address, role string) +} + +type RolesModuleFactory func(core IDAOCore) IRolesModule diff --git a/gno/p/dao_roles_group/gno.mod b/gno/p/dao_roles_group/gno.mod new file mode 100644 index 0000000000..6a06996be2 --- /dev/null +++ b/gno/p/dao_roles_group/gno.mod @@ -0,0 +1,8 @@ +module gno.land/p/teritori/dao_roles_group + +require ( + gno.land/p/demo/json v0.0.0-latest + gno.land/p/teritori/dao_interfaces v0.0.0-latest + gno.land/p/teritori/jsonutil v0.0.0-latest + gno.land/p/teritori/role_manager v0.0.0-latest +) diff --git a/gno/p/dao_roles_group/roles_group.gno b/gno/p/dao_roles_group/roles_group.gno new file mode 100644 index 0000000000..e10f45a40f --- /dev/null +++ b/gno/p/dao_roles_group/roles_group.gno @@ -0,0 +1,63 @@ +package dao_roles_group + +import ( + "std" + + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/jsonutil" + "gno.land/p/teritori/role_manager" +) + +type RolesGroup struct { + dao_interfaces.IRolesModule + + rm *role_manager.RoleManager +} + +func NewRolesGroup() *RolesGroup { + return &RolesGroup{ + rm: role_manager.NewWithAddress(std.PrevRealm().Addr()), + } +} + +func (r *RolesGroup) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "gno/p/teritori/dao_roles_group", + Version: "0.1.0", + } +} + +func (r *RolesGroup) ConfigJSON() string { + return json.ObjectNode("", map[string]*json.Node{ + "totalRoles": jsonutil.IntNode(r.rm.CountRoles()), + }).String() +} + +func (r *RolesGroup) Render(path string) string { + return "Not implemented yet" +} + +func (r *RolesGroup) HasRole(address std.Address, role string) bool { + return r.rm.HasRole(address, role) +} + +func (r *RolesGroup) NewRole(roleName string) { + r.rm.CreateNewRole(roleName, []string{}) +} + +func (r *RolesGroup) DeleteRole(roleName string) { + r.rm.DeleteRole(roleName) +} + +func (r *RolesGroup) GrantRole(address std.Address, role string) { + r.rm.AddRoleToUser(address, role) +} + +func (r *RolesGroup) RevokeRole(address std.Address, role string) { + r.rm.RemoveRoleFromUser(address, role) +} + +func (r *RolesGroup) GetMemberRoles(address std.Address) []string { + return r.rm.GetUserRoles(address) +} diff --git a/gno/p/role_manager/role_manager.gno b/gno/p/role_manager/role_manager.gno index 48d42025e7..7a7b91a0d8 100644 --- a/gno/p/role_manager/role_manager.gno +++ b/gno/p/role_manager/role_manager.gno @@ -163,6 +163,30 @@ func (rm *RoleManager) HasRole(user std.Address, roleName string) bool { return userRoles.Has(roleName) } +func (rm *RoleManager) IsRoleExist(roleName string) bool { + return rm.roles.Has(roleName) +} + +func (rm *RoleManager) CountRoles() int { + return rm.roles.Size() +} + +func (rm *RoleManager) GetUserRoles(user std.Address) []string { + userRoles, ok := rm.users.Get(user.String()) + if !ok { + return []string{} + } + i := 0 + roles := userRoles.(*avl.Tree) + res := make([]string, roles.Size()) + roles.Iterate("", "", func(key string, value interface{}) bool { + res[i] = key + i++ + return false + }) + return res +} + func (rm *RoleManager) mustGetRole(roleName string) *Role { role, ok := rm.roles.Get(roleName) if !ok { diff --git a/gno/r/dao_realm/dao_realm.gno b/gno/r/dao_realm/dao_realm.gno index 948a6d4b34..b45ee65012 100644 --- a/gno/r/dao_realm/dao_realm.gno +++ b/gno/r/dao_realm/dao_realm.gno @@ -7,6 +7,7 @@ import ( dao_core "gno.land/p/teritori/dao_core" dao_interfaces "gno.land/p/teritori/dao_interfaces" proposal_single "gno.land/p/teritori/dao_proposal_single" + "gno.land/p/teritori/dao_roles_group" "gno.land/p/teritori/dao_utils" voting_group "gno.land/p/teritori/dao_voting_group" "gno.land/r/demo/profile" @@ -20,6 +21,7 @@ import ( var ( daoCore dao_interfaces.IDAOCore group *voting_group.VotingGroup + roles *dao_roles_group.RolesGroup registered bool ) @@ -34,6 +36,18 @@ func init() { return group } + rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + roles = dao_roles_group.NewRolesGroup() + roles.NewRole("admin") + roles.NewRole("moderator") + roles.NewRole("member") + roles.GrantRole("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", "admin") + roles.GrantRole("g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", "moderator") + roles.GrantRole("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", "member") + roles.GrantRole("g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", "member") + return roles + } + // TODO: consider using factories that return multiple modules and handlers proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ @@ -73,7 +87,7 @@ func init() { }, } - daoCore = dao_core.NewDAOCore(votingModuleFactory, proposalModulesFactories, messageHandlersFactories) + daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) // Register the DAO profile profile.SetStringField(profile.DisplayName, "DAO Realm") diff --git a/gno/r/dao_realm/gno.mod b/gno/r/dao_realm/gno.mod index 29559da1ea..4a7e584b4b 100644 --- a/gno/r/dao_realm/gno.mod +++ b/gno/r/dao_realm/gno.mod @@ -5,6 +5,7 @@ require ( gno.land/p/teritori/dao_core v0.0.0-latest gno.land/p/teritori/dao_interfaces v0.0.0-latest gno.land/p/teritori/dao_proposal_single v0.0.0-latest + gno.land/p/teritori/dao_roles_group v0.0.0-latest gno.land/p/teritori/dao_utils v0.0.0-latest gno.land/p/teritori/dao_voting_group v0.0.0-latest gno.land/p/teritori/havl v0.0.0-latest diff --git a/networks.json b/networks.json index e330f26ff2..14cbdc8cff 100644 --- a/networks.json +++ b/networks.json @@ -4522,6 +4522,7 @@ "modboardsPkgPath": "gno.land/r/teritori/modboards", "groupsPkgPath": "gno.land/r/teritori/groups", "votingGroupPkgPath": "gno.land/p/teritori/dao_voting_group", + "rolesGroupPkgPath": "gno.land/p/teritori/dao_roles_group", "daoProposalSinglePkgPath": "gno.land/p/teritori/dao_proposal_single", "profilePkgPath": "gno.land/r/demo/profile", "daoInterfacesPkgPath": "gno.land/p/teritori/dao_interfaces", @@ -4573,6 +4574,7 @@ "socialFeedsDAOPkgPath": "gno.land/r/teritori/social_feeds_dao", "groupsPkgPath": "gno.land/r/teritori/groups", "votingGroupPkgPath": "gno.land/p/teritori/dao_voting_group", + "rolesGroupPkgPath": "gno.land/p/teritori/dao_roles_group", "daoProposalSinglePkgPath": "gno.land/p/teritori/dao_proposal_single", "daoInterfacesPkgPath": "gno.land/p/teritori/dao_interfaces", "daoCorePkgPath": "gno.land/p/teritori/dao_core", diff --git a/packages/components/dao/ConfigureVotingSection.tsx b/packages/components/dao/ConfigureVotingSection.tsx index f7ebc24fa9..cb87a14bf8 100644 --- a/packages/components/dao/ConfigureVotingSection.tsx +++ b/packages/components/dao/ConfigureVotingSection.tsx @@ -12,10 +12,7 @@ import { patternOnlyNumbers } from "../../utils/formRules"; import { neutral33, neutral77, neutralA3 } from "../../utils/style/colors"; import { fontSemibold14, fontSemibold28 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; -import { - ConfigureVotingFormType, - ORGANIZATION_DEPLOYER_STEPS, -} from "../../utils/types/organizations"; +import { ConfigureVotingFormType } from "../../utils/types/organizations"; import { BrandText } from "../BrandText"; import { RangeSlider } from "../RangeSlider"; import { PrimaryButton } from "../buttons/PrimaryButton"; @@ -24,6 +21,7 @@ import { SpacerColumn, SpacerRow } from "../spacer"; interface ConfigureVotingSectionProps { onSubmit: (form: ConfigureVotingFormType) => void; + steps: string[]; noDuration?: boolean; submitLabel?: string; contentContainerStyle?: StyleProp; @@ -31,6 +29,7 @@ interface ConfigureVotingSectionProps { export const ConfigureVotingSection: React.FC = ({ onSubmit, + steps, noDuration, submitLabel, contentContainerStyle, @@ -120,9 +119,10 @@ export const ConfigureVotingSection: React.FC = ({ diff --git a/packages/components/dao/DAOMembers.tsx b/packages/components/dao/DAOMembers.tsx index b4a5ef0593..0ead66f620 100644 --- a/packages/components/dao/DAOMembers.tsx +++ b/packages/components/dao/DAOMembers.tsx @@ -90,6 +90,7 @@ export const DAOMembers: React.FC<{ margin: halfGap, }} daoId={daoId} + roles={member.roles} /> ); })} diff --git a/packages/components/dao/GnoDemo.tsx b/packages/components/dao/GnoDemo.tsx index 4fed515cac..e4e9d31b2f 100644 --- a/packages/components/dao/GnoDemo.tsx +++ b/packages/components/dao/GnoDemo.tsx @@ -80,6 +80,7 @@ export const GnoDemo: React.FC<{ noDuration submitLabel="Propose settings change" contentContainerStyle={{ paddingHorizontal: 0 }} + steps={[]} /> ); diff --git a/packages/components/user/UserCard.tsx b/packages/components/user/UserCard.tsx index 27202e05e1..f921fcce2c 100644 --- a/packages/components/user/UserCard.tsx +++ b/packages/components/user/UserCard.tsx @@ -1,12 +1,12 @@ import { Buffer } from "buffer"; import React, { ComponentProps, useCallback } from "react"; import { - StyleProp, - ViewStyle, - View, - StyleSheet, Pressable, ScrollView, + StyleProp, + StyleSheet, + View, + ViewStyle, } from "react-native"; import { UserDisplayName } from "./UserDisplayName"; @@ -23,15 +23,15 @@ import { useNSUserInfo } from "../../hooks/useNSUserInfo"; import useSelectedWallet from "../../hooks/useSelectedWallet"; import { parseUserId } from "../../networks"; import { - neutral77, + neutral00, neutral33, + neutral77, neutralA3, - neutral00, } from "../../utils/style/colors"; import { - fontSemibold12, fontSemibold10, - fontSemibold8, + fontSemibold12, + fontSemibold9, } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; import { BrandText } from "../BrandText"; @@ -45,8 +45,9 @@ import { UserAvatarWithFrame } from "../images/AvatarWithFrame"; export const UserCard: React.FC<{ userId: string; style: StyleProp; + roles?: string[]; daoId?: string; -}> = ({ userId, style, daoId }) => { +}> = ({ userId, style, daoId, roles }) => { const [, userAddress] = parseUserId(userId); const { metadata } = useNSUserInfo(userId); const selectedWallet = useSelectedWallet(); @@ -114,48 +115,48 @@ export const UserCard: React.FC<{ - - Roles - - - {fakeRoles.map((role, index) => { - return ( - - - {role.text} - - - ); - })} - + {roles && roles.length && ( + + + Roles + + + {roles.map((role, index) => { + return ( + + + {role} + + + ); + })} + + + )} @@ -251,20 +252,6 @@ const FollowingFollowers: React.FC<{ style?: StyleProp }> = ({ ); }; -const fakeRoles: { highlight?: boolean; text: string }[] = [ - { - text: "Hiring", - highlight: true, - }, - { text: "Teritorian" }, - { text: "Torishark" }, - { text: "OG" }, - { text: "Ripper" }, - { text: "Squad leader" }, - { text: "NFT Enjoyoor" }, - { text: "Tester" }, -]; - const useProposeToRemoveMember = (daoId: string | undefined) => { const makeProposal = useDAOMakeProposal(daoId); const { data: groupAddress } = useDAOGroup(daoId); diff --git a/packages/hooks/dao/useDAOMembers.ts b/packages/hooks/dao/useDAOMembers.ts index 7e6954e3e6..3e857088f2 100644 --- a/packages/hooks/dao/useDAOMembers.ts +++ b/packages/hooks/dao/useDAOMembers.ts @@ -15,7 +15,8 @@ import { extractGnoJSONString } from "@/utils/gno"; type GnoDAOMember = { address: string; - weight: number; + power: number; + roles: string[]; }; export const useDAOMembers = (daoId: string | undefined) => { @@ -40,7 +41,11 @@ export const useDAOMembers = (daoId: string | undefined) => { daoGroupAddress, ); const { members } = await cw4Client.listMembers({ limit: 100 }); - return members; + return members.map((member) => ({ + addr: member.addr, + weight: member.weight, + roles: [], + })); } case NetworkKind.Gno: { if (!network.groupsPkgPath) { @@ -50,12 +55,13 @@ export const useDAOMembers = (daoId: string | undefined) => { const res: GnoDAOMember[] = extractGnoJSONString( await provider.evaluateExpression( daoAddress, - `daoCore.VotingModule().GetMembersJSON("", "", 0, 0)`, + `daoCore.GetMembersJSON("", "", 0, 0)`, ), ); return res.map((member) => ({ addr: member.address, - weight: member.weight, + weight: member.power, + roles: member.roles, })); } } diff --git a/packages/networks/gno-dev/index.ts b/packages/networks/gno-dev/index.ts index 8f1e2e06f9..1e8556e8b7 100644 --- a/packages/networks/gno-dev/index.ts +++ b/packages/networks/gno-dev/index.ts @@ -45,6 +45,7 @@ export const gnoDevNetwork: GnoNetworkInfo = { modboardsPkgPath: "gno.land/r/teritori/modboards", groupsPkgPath: "gno.land/r/teritori/groups", votingGroupPkgPath: "gno.land/p/teritori/dao_voting_group", + rolesGroupPkgPath: "gno.land/p/teritori/dao_roles_group", daoProposalSinglePkgPath: "gno.land/p/teritori/dao_proposal_single", profilePkgPath: "gno.land/r/demo/profile", daoInterfacesPkgPath: "gno.land/p/teritori/dao_interfaces", diff --git a/packages/networks/gno-portal/index.ts b/packages/networks/gno-portal/index.ts index a2fa0d2dce..767f1a3fb1 100644 --- a/packages/networks/gno-portal/index.ts +++ b/packages/networks/gno-portal/index.ts @@ -35,6 +35,7 @@ export const gnoPortalNetwork: GnoNetworkInfo = { // modboardsPkgPath: "gno.land/r/teritori/modboards_v4", groupsPkgPath: "gno.land/r/teritori/groups", votingGroupPkgPath: "gno.land/p/teritori/dao_voting_group", + rolesGroupPkgPath: "gno.land/p/teritori/dao_roles_group", daoProposalSinglePkgPath: "gno.land/p/teritori/dao_proposal_single", daoInterfacesPkgPath: "gno.land/p/teritori/dao_interfaces", daoCorePkgPath: "gno.land/p/teritori/dao_core", diff --git a/packages/networks/types.ts b/packages/networks/types.ts index b1be6efb2d..3167b7dae4 100644 --- a/packages/networks/types.ts +++ b/packages/networks/types.ts @@ -114,6 +114,7 @@ export type GnoNetworkInfo = NetworkInfoBase & { socialFeedsPkgPath?: string; socialFeedsDAOPkgPath?: string; votingGroupPkgPath?: string; + rolesGroupPkgPath?: string; daoProposalSinglePkgPath?: string; daoInterfacesPkgPath?: string; daoCorePkgPath?: string; diff --git a/packages/screens/Organizations/OrganizationDeployerScreen.tsx b/packages/screens/Organizations/OrganizationDeployerScreen.tsx index 39576d6884..a6bcbe9f85 100644 --- a/packages/screens/Organizations/OrganizationDeployerScreen.tsx +++ b/packages/screens/Organizations/OrganizationDeployerScreen.tsx @@ -1,264 +1,49 @@ -import { useQueryClient } from "@tanstack/react-query"; import React, { useState } from "react"; -import { StyleSheet, View } from "react-native"; +import { View } from "react-native"; import { CreateDAOSection } from "./components/CreateDAOSection"; -import { LaunchingOrganizationSection } from "./components/LaunchingOrganizationSection"; -import { MemberSettingsSection } from "./components/MemberSettingsSection"; -import { ReviewInformationSection } from "./components/ReviewInformationSection"; +import { MembershipDeployerSteps } from "./components/MembershipOrg/MembershipDeployerSteps"; import { RightSection } from "./components/RightSection"; -import { TokenSettingsSection } from "./components/TokenSettingsSection"; -import useSelectedWallet from "../../hooks/useSelectedWallet"; +import { RolesDeployerSteps } from "./components/RolesOrg/RolesDeployerSteps"; +import { TokenDeployerSteps } from "./components/TokenOrg/TokenDeployerSteps"; import { BrandText } from "@/components/BrandText"; import { ScreenContainer } from "@/components/ScreenContainer"; -import { ConfigureVotingSection } from "@/components/dao/ConfigureVotingSection"; -import { useFeedbacks } from "@/context/FeedbacksProvider"; -import { nsNameInfoQueryKey } from "@/hooks/useNSNameInfo"; import { - getNetwork, - getUserId, - mustGetCosmosNetwork, - NetworkKind, -} from "@/networks"; -import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; -import { - createDaoMemberBased, - CreateDaoMemberBasedParams, - createDaoTokenBased, -} from "@/utils/dao"; -import { adenaDeployGnoDAO } from "@/utils/gnodao/deploy"; -import { - ConfigureVotingFormType, CreateDaoFormType, DaoType, - LAUNCHING_PROCESS_STEPS, - MemberSettingFormType, - ORGANIZATION_DEPLOYER_STEPS, - TokenSettingFormType, + MEMBERSHIP_ORGANIZATION_DEPLOYER_STEPS, + ROLES_BASED_ORGANIZATION_STEPS, + TOKEN_ORGANIZATION_DEPLOYER_STEPS, } from "@/utils/types/organizations"; export const OrganizationDeployerScreen = () => { - const selectedWallet = useSelectedWallet(); - const { setToast } = useFeedbacks(); - const [daoAddress, setDAOAddress] = useState(""); + const [steps, setSteps] = useState(MEMBERSHIP_ORGANIZATION_DEPLOYER_STEPS); const [currentStep, setCurrentStep] = useState(0); - const [step1DaoInfoFormData, setStep1DaoInfoFormData] = - useState(); - const [step2ConfigureVotingFormData, setStep2ConfigureVotingFormData] = - useState(); - const [step3TokenSettingFormData, setStep3TokenSettingFormData] = - useState(); - const [step3MemberSettingFormData, setStep3MemberSettingFormData] = - useState(); - const queryClient = useQueryClient(); + const [daoInfoFormData, setdaoInfoFormData] = useState(); const [launchingStep, setLaunchingStep] = useState(0); - const getPercent = (num: number | undefined): string => { - let ret_num: number; - ret_num = num === undefined ? 0 : num; - ret_num = ret_num < 0 ? 0 : ret_num; - ret_num = ret_num > 100 ? 100 : ret_num; - return (ret_num / 100).toFixed(2); - }; - const getDuration = ( - days: string | undefined, - hours: string | undefined, - minutes: string | undefined, - ): number => { - const num_days = !days ? 0 : parseInt(days, 10); - const num_hours = !hours ? 0 : parseInt(hours, 10); - const num_minutes = !minutes ? 0 : parseInt(minutes, 10); - return num_days * 3600 * 24 + num_hours * 3600 + num_minutes * 60; - }; - - const network = getNetwork(selectedWallet?.networkId); + const [unlockedSteps, setUnlockedSteps] = useState([0]); - const createDaoContract = async (): Promise => { - try { - switch (network?.kind) { - case NetworkKind.Gno: { - const name = step1DaoInfoFormData?.associatedHandle!; - const pkgPath = await adenaDeployGnoDAO( - network.id, - selectedWallet?.address!, - { - name, - maxVotingPeriodSeconds: - parseInt(step2ConfigureVotingFormData?.days!, 10) * - 24 * - 60 * - 60, - initialMembers: (step3MemberSettingFormData?.members || []).map( - (member) => ({ - address: member.addr, - weight: parseInt(member.weight, 10), - }), - ), - thresholdPercent: - step2ConfigureVotingFormData?.minimumApprovalPercent!, - quorumPercent: step2ConfigureVotingFormData?.supportPercent!, - displayName: step1DaoInfoFormData?.organizationName!, - description: step1DaoInfoFormData?.organizationDescription!, - imageURI: step1DaoInfoFormData?.imageUrl!, - }, - ); - setLaunchingStep(1); - setDAOAddress(pkgPath); - return true; - } - case NetworkKind.Cosmos: { - if ( - !selectedWallet || - !step1DaoInfoFormData || - !step2ConfigureVotingFormData - ) { - return false; - } - - const networkId = selectedWallet.networkId; - const network = mustGetCosmosNetwork(networkId); - const cwAdminFactoryContractAddress = - network.cwAdminFactoryContractAddress!; - const walletAddress = selectedWallet.address; - const signingClient = await getKeplrSigningCosmWasmClient(networkId); - - let createDaoRes = null; - if (step1DaoInfoFormData.structure === DaoType.TOKEN_BASED) { - if (!step3TokenSettingFormData) return false; - createDaoRes = await createDaoTokenBased( - { - client: signingClient, - sender: walletAddress, - contractAddress: cwAdminFactoryContractAddress, - daoPreProposeSingleCodeId: network.daoPreProposeSingleCodeId!, - daoProposalSingleCodeId: network.daoProposalSingleCodeId!, - daoCw20CodeId: network.daoCw20CodeId!, - daoCw20StakeCodeId: network.daoCw20StakeCodeId!, - daoVotingCw20StakedCodeId: network.daoVotingCw20StakedCodeId!, - daoCoreCodeId: network.daoCoreCodeId!, - name: step1DaoInfoFormData.organizationName, - description: step1DaoInfoFormData.organizationDescription, - tns: step1DaoInfoFormData.associatedHandle, - imageUrl: step1DaoInfoFormData.imageUrl, - tokenName: step3TokenSettingFormData.tokenName, - tokenSymbol: step3TokenSettingFormData.tokenSymbol, - tokenHolders: step3TokenSettingFormData.tokenHolders.map( - (item) => { - return { address: item.address, amount: item.balance }; - }, - ), - quorum: getPercent(step2ConfigureVotingFormData.supportPercent), - threshold: getPercent( - step2ConfigureVotingFormData.minimumApprovalPercent, - ), - maxVotingPeriod: getDuration( - step2ConfigureVotingFormData.days, - step2ConfigureVotingFormData.hours, - step2ConfigureVotingFormData.minutes, - ), - }, - "auto", - ); - } else if (step1DaoInfoFormData.structure === DaoType.MEMBER_BASED) { - if (!step3MemberSettingFormData) return false; - const params: CreateDaoMemberBasedParams = { - networkId, - sender: walletAddress, - contractAddress: cwAdminFactoryContractAddress, - daoCoreCodeId: network.daoCoreCodeId!, - daoPreProposeSingleCodeId: network.daoPreProposeSingleCodeId!, - daoProposalSingleCodeId: network.daoProposalSingleCodeId!, - cw4GroupCodeId: network.cw4GroupCodeId!, - daoVotingCw4CodeId: network.daoVotingCw4CodeId!, - name: step1DaoInfoFormData.organizationName, - description: step1DaoInfoFormData.organizationDescription, - tns: step1DaoInfoFormData.associatedHandle, - imageUrl: step1DaoInfoFormData.imageUrl, - members: step3MemberSettingFormData.members.map((value) => ({ - addr: value.addr, - weight: parseInt(value.weight, 10), - })), - quorum: getPercent(step2ConfigureVotingFormData.supportPercent), - threshold: getPercent( - step2ConfigureVotingFormData.minimumApprovalPercent, - ), - maxVotingPeriod: getDuration( - step2ConfigureVotingFormData.days, - step2ConfigureVotingFormData.hours, - step2ConfigureVotingFormData.minutes, - ), - onStepChange: setLaunchingStep, - }; - const { daoAddress, executeResult } = await createDaoMemberBased( - params, - "auto", - ); - createDaoRes = executeResult; - setDAOAddress(daoAddress); - } else { - return false; - } - console.log("Res: ", createDaoRes); - console.log(createDaoRes.transactionHash); - if (createDaoRes) { - return true; - } else { - console.error("Failed to create DAO"); - setToast({ - title: "Failed to create DAO", - mode: "normal", - type: "error", - }); - return false; - } - } - default: { - throw new Error("Network not supported " + selectedWallet?.networkId); - } - } - } catch (err: unknown) { - console.error("Failed to create DAO: ", err); - if (err instanceof Error) { - setToast({ - title: "Failed to create DAO", - message: err.message, - mode: "normal", - type: "error", - }); - } - return false; + // functions + const onStructureChange = (structure: DaoType) => { + if (structure === DaoType.MEMBER_BASED) { + setSteps(MEMBERSHIP_ORGANIZATION_DEPLOYER_STEPS); + } + if (structure === DaoType.ROLES_BASED) { + setSteps(ROLES_BASED_ORGANIZATION_STEPS); } + if (structure === DaoType.TOKEN_BASED) { + setSteps(TOKEN_ORGANIZATION_DEPLOYER_STEPS); + } + setCurrentStep(0); + setUnlockedSteps([0]); }; - // functions + const onSubmitDAO = (data: CreateDaoFormType) => { - setStep1DaoInfoFormData(data); + setdaoInfoFormData(data); setCurrentStep(1); }; - const onSubmitConfigureVoting = (data: ConfigureVotingFormType) => { - setStep2ConfigureVotingFormData(data); - setCurrentStep(2); - }; - - const onSubmitTokenSettings = (data: TokenSettingFormType) => { - setStep3TokenSettingFormData(data); - setCurrentStep(3); - }; - - const onSubmitMemberSettings = (data: MemberSettingFormType) => { - const temp = data.members.filter((member) => member.addr !== undefined); - const processedData: MemberSettingFormType = { members: temp }; - setStep3MemberSettingFormData(processedData); - setCurrentStep(3); - }; - - const onStartLaunchingProcess = async () => { - setCurrentStep(4); - if (!(await createDaoContract())) { - setCurrentStep(3); - } - }; - return ( Organization Deployer} @@ -267,93 +52,66 @@ export const OrganizationDeployerScreen = () => { fullWidth noScroll > - - - - - - - - - - - - - + + - - - - - - - { - let tokenId = step1DaoInfoFormData?.associatedHandle; - const network = getNetwork(selectedWallet?.networkId); - if (network?.kind === NetworkKind.Gno) { - tokenId += ".gno"; - } else if (network?.kind === NetworkKind.Cosmos) { - tokenId += network.nameServiceTLD || ""; - } - await queryClient.invalidateQueries( - nsNameInfoQueryKey(selectedWallet?.networkId, tokenId), - ); - setStep1DaoInfoFormData(undefined); - setStep2ConfigureVotingFormData(undefined); - setStep3MemberSettingFormData(undefined); - setStep3TokenSettingFormData(undefined); - setCurrentStep(0); - setDAOAddress(""); - setLaunchingStep(0); - }} + {daoInfoFormData?.structure === DaoType.MEMBER_BASED && ( + - + )} + + {daoInfoFormData?.structure === DaoType.ROLES_BASED && ( + + )} + + {daoInfoFormData?.structure === DaoType.TOKEN_BASED && ( + + )} ); }; - -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - row: { flexDirection: "row", flex: 1 }, - fill: { flex: 1 }, - hidden: { display: "none" }, - show: { display: "flex", flex: 1 }, -}); diff --git a/packages/screens/Organizations/components/CreateDAOSection.tsx b/packages/screens/Organizations/components/CreateDAOSection.tsx index 1ed41a2c15..5c8fd851ed 100644 --- a/packages/screens/Organizations/components/CreateDAOSection.tsx +++ b/packages/screens/Organizations/components/CreateDAOSection.tsx @@ -10,26 +10,25 @@ import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { AvailableNamesInput } from "@/components/inputs/AvailableNamesInput"; import { TextInputCustom } from "@/components/inputs/TextInputCustom"; import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useDeveloperMode } from "@/hooks/useDeveloperMode"; import { useNSAvailability } from "@/hooks/useNSAvailability"; import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork"; import { NetworkKind } from "@/networks"; import { neutral33, neutral77 } from "@/utils/style/colors"; import { fontSemibold20, fontSemibold28 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; -import { - CreateDaoFormType, - DaoType, - ORGANIZATION_DEPLOYER_STEPS, -} from "@/utils/types/organizations"; - -//const RADIO_DESCRIPTION_TYPES = ["Membership", "Governance", "Decentralized"]; +import { CreateDaoFormType, DaoType } from "@/utils/types/organizations"; interface CreateDAOSectionProps { onSubmit: (form: CreateDaoFormType) => void; + onStructureChange: (structure: DaoType) => void; + organizationSteps: string[]; } export const CreateDAOSection: React.FC = ({ onSubmit, + onStructureChange, + organizationSteps, }) => { const { control, @@ -49,6 +48,7 @@ export const CreateDAOSection: React.FC = ({ const selectedRadioStructure = watch("structure"); const uri = watch("imageUrl"); const name = watch("associatedHandle"); + const [isDev] = useDeveloperMode(); const nameAvailability = useNSAvailability(selectedNetwork?.id, name); @@ -60,6 +60,11 @@ export const CreateDAOSection: React.FC = ({ const onErrorImageLoading = () => setError("imageUrl", { type: "pattern", message: "This image is invalid" }); + const onStructureChangeHandler = (structure: DaoType) => { + setValue("structure", structure); + onStructureChange(structure); + }; + return ( @@ -142,7 +147,7 @@ export const CreateDAOSection: React.FC = ({ setValue("structure", DaoType.MEMBER_BASED)} + onPress={() => onStructureChangeHandler(DaoType.MEMBER_BASED)} title="Membership-based TORG - Teritori Organization" description="Small organization with a few members who are likely to stick around. Members can be added and removed by a vote of existing members." /> @@ -152,19 +157,32 @@ export const CreateDAOSection: React.FC = ({ setValue("structure", DaoType.TOKEN_BASED)} + onPress={() => onStructureChangeHandler(DaoType.TOKEN_BASED)} title="Governance Token-based TORG - Teritori Organization" description="Fluid organization with many members who leave and join frequently. Members can join and leave by exchanging governance shares." /> + + + onStructureChangeHandler(DaoType.ROLES_BASED)} + title="Roles-based TORG - Teritori Organization" + description="Organization where members have roles and permissions. Roles can be assigned and removed through a vote of existing members." + /> + + + + = ({ }) => { return ( - + {uri ? ( ) : ( - + )} - Preview + Preview ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - imagePreviewer: { - height: 140, - width: 140, - borderRadius: 12, - borderWidth: 1, - borderColor: neutral33, - backgroundColor: neutral22, - justifyContent: "center", - alignItems: "center", - marginBottom: layout.spacing_x1_5, - overflow: "hidden", - }, - image: { - height: 140, - width: 140, - borderRadius: 12, - }, - text: StyleSheet.flatten([ - fontSemibold14, - { color: neutralA3, textAlign: "center" }, - ]), -}); +const imagePreviewerCStyle: ViewStyle = { + height: 140, + width: 140, + borderRadius: 12, + borderWidth: 1, + borderColor: neutral33, + backgroundColor: neutral22, + justifyContent: "center", + alignItems: "center", + marginBottom: layout.spacing_x1_5, + overflow: "hidden", +}; + +const imageCStyle: ImageStyle = { + height: 140, + width: 140, + borderRadius: 12, +}; + +const textCStyle: TextStyle = { + ...fontSemibold14, + color: neutralA3, + textAlign: "center", +}; diff --git a/packages/screens/Organizations/components/LaunchingOrganizationSection.tsx b/packages/screens/Organizations/components/LaunchingOrganizationSection.tsx index d31bfe29a2..bca7e500b8 100644 --- a/packages/screens/Organizations/components/LaunchingOrganizationSection.tsx +++ b/packages/screens/Organizations/components/LaunchingOrganizationSection.tsx @@ -1,6 +1,6 @@ import Lottie from "lottie-react-native"; import React, { useEffect, useRef } from "react"; -import { Animated, StyleSheet, View } from "react-native"; +import { Animated, View, ViewStyle } from "react-native"; import { BrandText } from "@/components/BrandText"; import { PrimaryButton } from "@/components/buttons/PrimaryButton"; @@ -37,7 +37,7 @@ export const LaunchingOrganizationSection: React.FC<{ }); return ( - + {isLaunched ? "All done" : "Launch organization"} @@ -48,7 +48,7 @@ export const LaunchingOrganizationSection: React.FC<{ Your organization is ready! - + - + void; + launchingStep: number; + setLaunchingStep: (step: number) => void; + organizationData: CreateDaoFormType | undefined; + setOrganizationData: (data: CreateDaoFormType | undefined) => void; +}> = ({ + currentStep, + setCurrentStep, + launchingStep, + setLaunchingStep, + organizationData, + setOrganizationData, +}) => { + const selectedWallet = useSelectedWallet(); + const [daoAddress, setDAOAddress] = useState(""); + const network = getNetwork(selectedWallet?.networkId); + const queryClient = useQueryClient(); + const { setToast } = useFeedbacks(); + const [configureVotingFormData, setConfigureVotingFormData] = + useState(); + const [memberSettingsFormData, setMemberSettingsFormData] = + useState(); + + const createDaoContract = async (): Promise => { + try { + switch (network?.kind) { + case NetworkKind.Gno: { + const name = organizationData?.associatedHandle!; + const pkgPath = await adenaDeployGnoDAO( + network.id, + selectedWallet?.address!, + { + name, + maxVotingPeriodSeconds: + parseInt(configureVotingFormData?.days!, 10) * 24 * 60 * 60, + roles: undefined, + initialMembers: (memberSettingsFormData?.members || []).map( + (member) => ({ + address: member.addr, + weight: parseInt(member.weight, 10), + roles: [], + }), + ), + thresholdPercent: + configureVotingFormData?.minimumApprovalPercent!, + quorumPercent: configureVotingFormData?.supportPercent!, + displayName: organizationData?.organizationName!, + description: organizationData?.organizationDescription!, + imageURI: organizationData?.imageUrl!, + }, + ); + setLaunchingStep(1); + setDAOAddress(pkgPath); + return true; + } + case NetworkKind.Cosmos: { + if ( + !selectedWallet || + !organizationData || + !configureVotingFormData + ) { + return false; + } + + const networkId = selectedWallet.networkId; + const network = mustGetCosmosNetwork(networkId); + const cwAdminFactoryContractAddress = + network.cwAdminFactoryContractAddress!; + const walletAddress = selectedWallet.address; + + if (!memberSettingsFormData) return false; + const params: CreateDaoMemberBasedParams = { + networkId, + sender: walletAddress, + contractAddress: cwAdminFactoryContractAddress, + daoCoreCodeId: network.daoCoreCodeId!, + daoPreProposeSingleCodeId: network.daoPreProposeSingleCodeId!, + daoProposalSingleCodeId: network.daoProposalSingleCodeId!, + cw4GroupCodeId: network.cw4GroupCodeId!, + daoVotingCw4CodeId: network.daoVotingCw4CodeId!, + name: organizationData.organizationName, + description: organizationData.organizationDescription, + tns: organizationData.associatedHandle, + imageUrl: organizationData.imageUrl, + members: memberSettingsFormData.members.map((value) => ({ + addr: value.addr, + weight: parseInt(value.weight, 10), + })), + quorum: getPercent(configureVotingFormData.supportPercent), + threshold: getPercent( + configureVotingFormData.minimumApprovalPercent, + ), + maxVotingPeriod: getDuration( + configureVotingFormData.days, + configureVotingFormData.hours, + configureVotingFormData.minutes, + ), + onStepChange: setLaunchingStep, + }; + const { daoAddress, executeResult } = await createDaoMemberBased( + params, + "auto", + ); + const createDaoRes = executeResult; + setDAOAddress(daoAddress); + if (createDaoRes) { + return true; + } else { + console.error("Failed to create DAO"); + setToast({ + title: "Failed to create DAO", + mode: "normal", + type: "error", + }); + return false; + } + } + default: { + throw new Error("Network not supported " + selectedWallet?.networkId); + } + } + } catch (err: unknown) { + console.error("Failed to create DAO: ", err); + if (err instanceof Error) { + setToast({ + title: "Failed to create DAO", + message: err.message, + mode: "normal", + type: "error", + }); + } + return false; + } + }; + + // functions + const onSubmitConfigureVoting = (data: ConfigureVotingFormType) => { + setConfigureVotingFormData(data); + setCurrentStep(2); + }; + + const onSubmitMemberSettings = (data: MembershipMemberSettingFormType) => { + setMemberSettingsFormData(data); + setCurrentStep(3); + }; + + const onStartLaunchingProcess = async () => { + setCurrentStep(4); + if (!(await createDaoContract())) { + setCurrentStep(3); + } + }; + + const resetForm = async () => { + let tokenId = organizationData?.associatedHandle; + const network = getNetwork(selectedWallet?.networkId); + if (network?.kind === NetworkKind.Gno) { + tokenId += ".gno"; + } else if (network?.kind === NetworkKind.Cosmos) { + tokenId += network.nameServiceTLD || ""; + } + await queryClient.invalidateQueries( + nsNameInfoQueryKey(selectedWallet?.networkId, tokenId), + ); + setOrganizationData(undefined); + setConfigureVotingFormData(undefined); + setMemberSettingsFormData(undefined); + setCurrentStep(0); + setDAOAddress(""); + setLaunchingStep(0); + }; + + return ( + <> + + + + + + + + + + + + + + ); +}; diff --git a/packages/screens/Organizations/components/MemberSettingsSection.tsx b/packages/screens/Organizations/components/MembershipOrg/MembershipMemberSettingsSection.tsx similarity index 59% rename from packages/screens/Organizations/components/MemberSettingsSection.tsx rename to packages/screens/Organizations/components/MembershipOrg/MembershipMemberSettingsSection.tsx index 6c44dc3d40..31bdc17a8d 100644 --- a/packages/screens/Organizations/components/MemberSettingsSection.tsx +++ b/packages/screens/Organizations/components/MembershipOrg/MembershipMemberSettingsSection.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { Pressable, ScrollView, StyleSheet, View } from "react-native"; +import { Pressable, ScrollView, View, ViewStyle } from "react-native"; -import trashSVG from "../../../../assets/icons/trash.svg"; -import walletInputSVG from "../../../../assets/icons/wallet-input.svg"; -import useSelectedWallet from "../../../hooks/useSelectedWallet"; +import trashSVG from "../../../../../assets/icons/trash.svg"; +import walletInputSVG from "../../../../../assets/icons/wallet-input.svg"; +import useSelectedWallet from "../../../../hooks/useSelectedWallet"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; @@ -13,23 +13,23 @@ import { SecondaryButton } from "@/components/buttons/SecondaryButton"; import { TextInputCustom } from "@/components/inputs/TextInputCustom"; import { SpacerColumn, SpacerRow } from "@/components/spacer"; import { patternOnlyNumbers, validateAddress } from "@/utils/formRules"; -import { neutral33, neutralA3 } from "@/utils/style/colors"; -import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { neutral33 } from "@/utils/style/colors"; +import { fontSemibold28 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; import { - MemberSettingFormType, - ORGANIZATION_DEPLOYER_STEPS, + MEMBERSHIP_ORGANIZATION_DEPLOYER_STEPS, + MembershipMemberSettingFormType, } from "@/utils/types/organizations"; -interface MemberSettingsSectionProps { - onSubmit: (form: MemberSettingFormType) => void; +interface MembershipMemberSettingsSectionProps { + onSubmit: (form: MembershipMemberSettingFormType) => void; } -export const MemberSettingsSection: React.FC = ({ - onSubmit, -}) => { +export const MembershipMemberSettingsSection: React.FC< + MembershipMemberSettingsSectionProps +> = ({ onSubmit }) => { const { handleSubmit, control, resetField } = - useForm(); + useForm(); // this effect put the selected wallet address in the first field only on initial load const selectedWallet = useSelectedWallet(); @@ -62,15 +62,15 @@ export const MemberSettingsSection: React.FC = ({ }; return ( - - + + Members {addressIndexes.map((id, index) => ( - - - + + + name={`members.${index}.addr`} noBrokenCorners label="Member Address" @@ -81,7 +81,7 @@ export const MemberSettingsSection: React.FC = ({ iconSVG={walletInputSVG} > removeAddressField(id)} > @@ -89,8 +89,8 @@ export const MemberSettingsSection: React.FC = ({ - - + + name={`members.${index}.weight`} noBrokenCorners label="Weight" @@ -107,10 +107,10 @@ export const MemberSettingsSection: React.FC = ({ - + @@ -119,41 +119,37 @@ export const MemberSettingsSection: React.FC = ({ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { - padding: layout.contentSpacing, - paddingRight: layout.spacing_x2_5, - paddingTop: layout.topContentSpacingWithHeading, - }, - voteText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutralA3, - }, - ]), - leftInput: { flex: 4 }, - rightInput: { flex: 1 }, - inputContainer: { - flexDirection: "row", - marginBottom: layout.spacing_x2, - }, - trashContainer: { - height: 16, - width: 16, - justifyContent: "center", - alignItems: "center", - borderRadius: 10, - backgroundColor: "rgba(244, 111, 118, 0.1)", - }, - fill: { flex: 1 }, - footer: { - justifyContent: "flex-end", - alignItems: "flex-end", - paddingVertical: layout.spacing_x1_5, - paddingHorizontal: layout.spacing_x2_5, - borderTopWidth: 1, - borderColor: neutral33, - }, -}); +const containerCStyle: ViewStyle = { + padding: layout.contentSpacing, + paddingRight: layout.spacing_x2_5, + paddingTop: layout.topContentSpacingWithHeading, +}; + +const leftInputCStyle: ViewStyle = { flex: 4 }; + +const rightInputCStyle: ViewStyle = { flex: 1 }; + +const inputContainerCStyle: ViewStyle = { + flexDirection: "row", + marginBottom: layout.spacing_x2, +}; + +const trashContainerCStyle: ViewStyle = { + height: 16, + width: 16, + justifyContent: "center", + alignItems: "center", + borderRadius: 10, + backgroundColor: "rgba(244, 111, 118, 0.1)", +}; + +const fillCStyle: ViewStyle = { flex: 1 }; + +const footerCStyle: ViewStyle = { + justifyContent: "flex-end", + alignItems: "flex-end", + paddingVertical: layout.spacing_x1_5, + paddingHorizontal: layout.spacing_x2_5, + borderTopWidth: 1, + borderColor: neutral33, +}; diff --git a/packages/screens/Organizations/components/MembershipOrg/MembershipReviewInformationSection.tsx b/packages/screens/Organizations/components/MembershipOrg/MembershipReviewInformationSection.tsx new file mode 100644 index 0000000000..c2f32f0b28 --- /dev/null +++ b/packages/screens/Organizations/components/MembershipOrg/MembershipReviewInformationSection.tsx @@ -0,0 +1,220 @@ +import React, { useCallback } from "react"; +import { + Image, + ImageStyle, + ScrollView, + TextStyle, + View, + ViewStyle, +} from "react-native"; + +import { ReviewCollapsable } from "../ReviewCollapsable"; +import { ReviewCollapsableItem } from "../ReviewCollapsableItem"; + +import { BrandText } from "@/components/BrandText"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork"; +import { NetworkKind } from "@/networks"; +import { neutral00, primaryColor } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { tinyAddress } from "@/utils/text"; +import { + ConfigureVotingFormType, + CreateDaoFormType, + MembershipMemberSettingFormType, +} from "@/utils/types/organizations"; + +interface MembershipReviewInformationSectionProps { + organizationData?: CreateDaoFormType; + votingSettingData?: ConfigureVotingFormType; + memberSettingData?: MembershipMemberSettingFormType; + onSubmit: () => void; +} + +export const MembershipReviewInformationSection: React.FC< + MembershipReviewInformationSectionProps +> = ({ organizationData, votingSettingData, memberSettingData, onSubmit }) => { + const network = useSelectedNetworkInfo(); + + const MemberReviewValue = useCallback( + ({ address, weight }: { address: string; weight: string }) => ( + + + {tinyAddress(address, 16)} + + + {weight} + + ), + [], + ); + + let associateName = ""; + if (network?.kind === NetworkKind.Gno) { + associateName = "gno.land/r/demo/" + organizationData?.associatedHandle; + } else if (network?.kind === NetworkKind.Cosmos) { + associateName = + organizationData?.associatedHandle || "" + network.nameServiceTLD; + } + + let price = "0"; + const availability = organizationData?.nameAvailability; + if ( + availability && + (availability.availability === "mint" || + availability.availability === "market") + ) { + price = availability.prettyPrice; + } + + return ( + + Review information + + + + ( + + )} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + {memberSettingData?.members.map((member, index) => ( + + ( + + )} + /> + + ))} + + + + + + + Associated Handle: + + + {associateName} + + + + + + + + Price of Organization Deployment: + + + + {price} + + + + + + + ); +}; + +const containerCStyle: ViewStyle = { + padding: layout.contentSpacing, + paddingRight: layout.spacing_x2_5, + paddingTop: layout.topContentSpacingWithHeading, +}; + +const rowCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", +}; + +const rowSBCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", +}; + +const imageCStyle: ImageStyle = { + width: 140, + height: 140, + borderRadius: 12, +}; + +const footerRowInsideCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + height: 24, + flexWrap: "wrap", +}; + +const addressTextCStyle: TextStyle = { + ...fontSemibold14, + padding: layout.spacing_x1, + backgroundColor: neutral00, + borderRadius: 8, +}; + +const fillCStyle: ViewStyle = { flex: 1 }; diff --git a/packages/screens/Organizations/components/RadioDescriptionSelector.tsx b/packages/screens/Organizations/components/RadioDescriptionSelector.tsx index 504ade8981..b711c21de6 100644 --- a/packages/screens/Organizations/components/RadioDescriptionSelector.tsx +++ b/packages/screens/Organizations/components/RadioDescriptionSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Pressable, StyleSheet, View } from "react-native"; +import { Pressable, View, ViewStyle } from "react-native"; import { BrandText } from "@/components/BrandText"; import { RadioButton } from "@/components/RadioButton"; @@ -17,11 +17,11 @@ export const RadioDescriptionSelector: React.FC<{ }> = ({ selected, disabled, onPress, title, description }) => { return ( - + {title} @@ -34,14 +34,11 @@ export const RadioDescriptionSelector: React.FC<{ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { - borderRadius: 12, - borderColor: neutral33, - borderWidth: 1, - padding: layout.spacing_x2_5, - }, - row: { flexDirection: "row", alignItems: "center" }, -}); +const containerCStyle: ViewStyle = { + borderRadius: 12, + borderColor: neutral33, + borderWidth: 1, + padding: layout.spacing_x2_5, +}; + +const rowCStyle: ViewStyle = { flexDirection: "row" }; diff --git a/packages/screens/Organizations/components/ReviewCollapsable.tsx b/packages/screens/Organizations/components/ReviewCollapsable.tsx index 4e6d95888c..00825ab7e7 100644 --- a/packages/screens/Organizations/components/ReviewCollapsable.tsx +++ b/packages/screens/Organizations/components/ReviewCollapsable.tsx @@ -1,5 +1,5 @@ import React, { ReactNode, useEffect, useRef, useState } from "react"; -import { Pressable, StyleSheet, View } from "react-native"; +import { Pressable, View, ViewStyle } from "react-native"; import Animated, { Extrapolate, interpolate, @@ -66,15 +66,15 @@ export const ReviewCollapsable: React.FC = ({ }; return ( - - - + + + {title} - + = ({ /> - + = ({ layout: { height: h }, }, }) => (heightRef.current = h)} - style={[styles.childrenContainer, styles.childInsideContainer]} + style={[childrenContainerCStyle, childInsideContainerCStyle]} > {children} @@ -100,32 +100,37 @@ export const ReviewCollapsable: React.FC = ({ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { borderRadius: 12, borderWidth: 1, borderColor: neutral33 }, - header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - width: "100%", - padding: layout.spacing_x2, - }, - rowWithCenter: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - }, - chevronContainer: { - alignItems: "center", - justifyContent: "center", - }, - childrenContainer: { - width: "100%", - }, - childInsideContainer: { - padding: layout.spacing_x1, - borderTopWidth: 1, - borderColor: neutral33, - }, -}); +const containerCStyle: ViewStyle = { + borderRadius: 12, + borderWidth: 1, + borderColor: neutral33, +}; + +const headerCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + padding: layout.spacing_x2, +}; + +const rowWithCenterCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", +}; + +const chevronContainerCStyle: ViewStyle = { + alignItems: "center", + justifyContent: "center", +}; + +const childrenContainerCStyle: ViewStyle = { + width: "100%", +}; + +const childInsideContainerCStyle: ViewStyle = { + padding: layout.spacing_x1, + borderTopWidth: 1, + borderColor: neutral33, +}; diff --git a/packages/screens/Organizations/components/ReviewCollapsableItem.tsx b/packages/screens/Organizations/components/ReviewCollapsableItem.tsx index c332b8b048..50dd054e18 100644 --- a/packages/screens/Organizations/components/ReviewCollapsableItem.tsx +++ b/packages/screens/Organizations/components/ReviewCollapsableItem.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { StyleSheet, View } from "react-native"; +import { TextStyle, View, ViewStyle } from "react-native"; import { BrandText } from "@/components/BrandText"; import { SpacerColumn } from "@/components/spacer"; @@ -17,11 +17,11 @@ export const ReviewCollapsableItem: React.FC = ({ value, }) => { return ( - - {title} + + {title} {typeof value === "string" ? ( - {value} + {value} ) : ( value && value() )} @@ -29,13 +29,16 @@ export const ReviewCollapsableItem: React.FC = ({ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { - backgroundColor: neutral17, - padding: layout.spacing_x1_5, - }, - title: StyleSheet.flatten([fontSemibold12, { color: neutralA3 }]), - value: StyleSheet.flatten([fontSemibold14]), -}); +const containerCStyle: ViewStyle = { + backgroundColor: neutral17, + padding: layout.spacing_x1_5, +}; + +const titleCStyle: TextStyle = { + ...fontSemibold12, + color: neutralA3, +}; + +const valueCStyle: TextStyle = { + ...fontSemibold14, +}; diff --git a/packages/screens/Organizations/components/RightSection.tsx b/packages/screens/Organizations/components/RightSection.tsx index 225e79e4f2..55e4b63906 100644 --- a/packages/screens/Organizations/components/RightSection.tsx +++ b/packages/screens/Organizations/components/RightSection.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { ActivityIndicator, Animated, Pressable, - StyleSheet, + TextStyle, View, + ViewStyle, } from "react-native"; import checkCircleSVG from "../../../../assets/icons/check-circle.svg"; @@ -32,6 +33,8 @@ interface RightSectionProps { onStepChange: (step: number) => void; isLaunching?: boolean; launchingCompleteStep?: number; + unlockedSteps: number[]; + setUnlockedSteps: React.Dispatch>; } export const RightSection: React.FC = ({ @@ -40,8 +43,9 @@ export const RightSection: React.FC = ({ onStepChange, isLaunching, launchingCompleteStep, + unlockedSteps, + setUnlockedSteps, }) => { - const [unlockedSteps, setUnlockedSteps] = useState([0]); const loadingPercentAnim = useRef(new Animated.Value(0)).current; const percentage = isLaunching @@ -49,7 +53,7 @@ export const RightSection: React.FC = ({ ? launchingCompleteStep / LAUNCHING_PROCESS_STEPS.length : 0 : unlockedSteps.length / steps.length; - const percentageText = `${percentage * 100}%`; + const percentageText = `${(percentage * 100).toFixed(2)}%`; const loadingWidth = loadingPercentAnim.interpolate({ inputRange: [0, 1], @@ -62,7 +66,7 @@ export const RightSection: React.FC = ({ setUnlockedSteps((u) => !u.includes(currentStep) ? [...u, currentStep] : u, ); - }, [currentStep]); + }, [currentStep, setUnlockedSteps]); useEffect(() => { Animated.timing(loadingPercentAnim, { @@ -74,7 +78,7 @@ export const RightSection: React.FC = ({ const SignatureProcess = useCallback( ({ title, completeText, isComplete }: LaunchingProcessStepType) => ( - + {isComplete ? ( ) : ( @@ -112,22 +116,22 @@ export const RightSection: React.FC = ({ ); return ( - - - + + + {isLaunching ? "Launching your organization" : percentageText} {!isLaunching && ( - + {`${currentStep + 1}/${steps.length}`} )} - + - - + + {isLaunching ? "Signature process" : `STEPS`} {isLaunching && launchingCompleteStep !== undefined @@ -143,7 +147,7 @@ export const RightSection: React.FC = ({ : steps.map((step, index) => ( = ({ > = ({ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { - width: 300, - height: "100%", - borderLeftWidth: 1, - borderColor: neutral33, - }, - topSection: { - height: 64, - position: "relative", - borderBottomWidth: 1, - borderColor: neutral33, - justifyContent: "center", - }, - topRow: { - justifyContent: "space-between", - flexDirection: "row", - paddingHorizontal: layout.spacing_x2, - }, - signatureProcess: { - flexDirection: "row", - alignItems: "center", - marginBottom: layout.spacing_x1_5, - }, - progressText: StyleSheet.flatten([fontSemibold12, { color: neutral77 }]), - stepsText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutral77, - marginBottom: layout.spacing_x2_5, - textTransform: "uppercase", - }, - ]), - progressBar: { - position: "absolute", - bottom: 0, - backgroundColor: primaryColor, - height: 2, - }, - step: { marginBottom: layout.spacing_x2_5 }, - stepText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutralA3, - }, - ]), - section: { padding: layout.spacing_x2 }, -}); +const containerCStyle: ViewStyle = { + width: 300, + height: "100%", + borderLeftWidth: 1, + borderColor: neutral33, +}; + +const topSectionCStyle: ViewStyle = { + height: 64, + position: "relative", + borderBottomWidth: 1, + borderColor: neutral33, + justifyContent: "center", +}; + +const topRowCStyle: ViewStyle = { + justifyContent: "space-between", + flexDirection: "row", + paddingHorizontal: layout.spacing_x2, +}; + +const signatureProcessCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + marginBottom: layout.spacing_x1_5, +}; + +const progressTextCStyle: TextStyle = { + ...fontSemibold12, + color: neutral77, +}; + +const stepsTextCStyle: TextStyle = { + ...fontSemibold14, + color: neutral77, + marginBottom: layout.spacing_x2_5, + textTransform: "uppercase", +}; + +const progressBarCStyle: ViewStyle = { + position: "absolute", + bottom: 0, + backgroundColor: primaryColor, + height: 2, +}; + +const stepCStyle: ViewStyle = { + marginBottom: layout.spacing_x2_5, +}; + +const stepTextCStyle: TextStyle = { + ...fontSemibold14, + color: neutralA3, +}; + +const sectionCStyle: ViewStyle = { + padding: layout.spacing_x2, +}; diff --git a/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx new file mode 100644 index 0000000000..20ad91e8f3 --- /dev/null +++ b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx @@ -0,0 +1,272 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { View } from "react-native"; + +import { RolesMembersSettingsSection } from "./RolesMembersSettingsSection"; +import { RolesReviewInformationSection } from "./RolesReviewInformationSection"; +import { RolesSettingsSection } from "./RolesSettingsSection"; +import { LaunchingOrganizationSection } from "../LaunchingOrganizationSection"; + +import { ConfigureVotingSection } from "@/components/dao/ConfigureVotingSection"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { nsNameInfoQueryKey } from "@/hooks/useNSNameInfo"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { + getNetwork, + getUserId, + mustGetCosmosNetwork, + NetworkKind, +} from "@/networks"; +import { createDaoMemberBased, CreateDaoMemberBasedParams } from "@/utils/dao"; +import { adenaDeployGnoDAO } from "@/utils/gnodao/deploy"; +import { getDuration, getPercent } from "@/utils/gnodao/helpers"; +import { + ConfigureVotingFormType, + CreateDaoFormType, + LAUNCHING_PROCESS_STEPS, + ROLES_BASED_ORGANIZATION_STEPS, + RolesMemberSettingFormType, + RolesSettingFormType, +} from "@/utils/types/organizations"; + +export const RolesDeployerSteps: React.FC<{ + currentStep: number; + setCurrentStep: (step: number) => void; + launchingStep: number; + setLaunchingStep: (step: number) => void; + organizationData: CreateDaoFormType | undefined; + setOrganizationData: (data: CreateDaoFormType | undefined) => void; +}> = ({ + currentStep, + setCurrentStep, + launchingStep, + setLaunchingStep, + organizationData, + setOrganizationData, +}) => { + const selectedWallet = useSelectedWallet(); + const [daoAddress, setDAOAddress] = useState(""); + const network = getNetwork(selectedWallet?.networkId); + const queryClient = useQueryClient(); + const { setToast } = useFeedbacks(); + const [configureVotingFormData, setConfigureVotingFormData] = + useState(); + const [rolesSettingsFormData, setRolesSettingsFormData] = + useState(); + const [memberSettingsFormData, setMemberSettingsFormData] = + useState(); + + const createDaoContract = async (): Promise => { + try { + switch (network?.kind) { + case NetworkKind.Gno: { + const name = organizationData?.associatedHandle!; + const roles = + rolesSettingsFormData?.roles?.map((role) => role.name.trim()) || []; + const initialMembers = (memberSettingsFormData?.members || []).map( + (member) => ({ + address: member.addr, + weight: parseInt(member.weight, 10), + roles: member.roles + ? member.roles.split(",").map((role) => role.trim()) + : [], + }), + ); + const pkgPath = await adenaDeployGnoDAO( + network.id, + selectedWallet?.address!, + { + name, + maxVotingPeriodSeconds: + parseInt(configureVotingFormData?.days!, 10) * 24 * 60 * 60, + roles, + initialMembers, + thresholdPercent: + configureVotingFormData?.minimumApprovalPercent!, + quorumPercent: configureVotingFormData?.supportPercent!, + displayName: organizationData?.organizationName!, + description: organizationData?.organizationDescription!, + imageURI: organizationData?.imageUrl!, + }, + ); + setLaunchingStep(1); + setDAOAddress(pkgPath); + return true; + } + case NetworkKind.Cosmos: { + if ( + !selectedWallet || + !organizationData || + !configureVotingFormData + ) { + return false; + } + + const networkId = selectedWallet.networkId; + const network = mustGetCosmosNetwork(networkId); + const cwAdminFactoryContractAddress = + network.cwAdminFactoryContractAddress!; + const walletAddress = selectedWallet.address; + + if (!memberSettingsFormData) return false; + const params: CreateDaoMemberBasedParams = { + networkId, + sender: walletAddress, + contractAddress: cwAdminFactoryContractAddress, + daoCoreCodeId: network.daoCoreCodeId!, + daoPreProposeSingleCodeId: network.daoPreProposeSingleCodeId!, + daoProposalSingleCodeId: network.daoProposalSingleCodeId!, + cw4GroupCodeId: network.cw4GroupCodeId!, + daoVotingCw4CodeId: network.daoVotingCw4CodeId!, + name: organizationData.organizationName, + description: organizationData.organizationDescription, + tns: organizationData.associatedHandle, + imageUrl: organizationData.imageUrl, + members: memberSettingsFormData.members.map((value) => ({ + addr: value.addr, + weight: parseInt(value.weight, 10), + })), + quorum: getPercent(configureVotingFormData.supportPercent), + threshold: getPercent( + configureVotingFormData.minimumApprovalPercent, + ), + maxVotingPeriod: getDuration( + configureVotingFormData.days, + configureVotingFormData.hours, + configureVotingFormData.minutes, + ), + onStepChange: setLaunchingStep, + }; + const { daoAddress, executeResult } = await createDaoMemberBased( + params, + "auto", + ); + const createDaoRes = executeResult; + setDAOAddress(daoAddress); + if (createDaoRes) { + return true; + } else { + console.error("Failed to create DAO"); + setToast({ + title: "Failed to create DAO", + mode: "normal", + type: "error", + }); + return false; + } + } + default: { + throw new Error("Network not supported " + selectedWallet?.networkId); + } + } + } catch (err: unknown) { + console.error("Failed to create DAO: ", err); + if (err instanceof Error) { + setToast({ + title: "Failed to create DAO", + message: err.message, + mode: "normal", + type: "error", + }); + } + return false; + } + }; + + // functions + const onSubmitConfigureVoting = (data: ConfigureVotingFormType) => { + setConfigureVotingFormData(data); + setCurrentStep(2); + }; + + const onSubmitRolesSettings = (data: RolesSettingFormType) => { + setRolesSettingsFormData(data); + setCurrentStep(3); + }; + + const onSubmitMemberSettings = (data: RolesMemberSettingFormType) => { + setMemberSettingsFormData(data); + setCurrentStep(4); + }; + + const onStartLaunchingProcess = async () => { + setCurrentStep(5); + if (!(await createDaoContract())) { + setCurrentStep(4); + resetForm(); + } + }; + + const resetForm = async () => { + let tokenId = organizationData?.associatedHandle; + const network = getNetwork(selectedWallet?.networkId); + if (network?.kind === NetworkKind.Gno) { + tokenId += ".gno"; + } else if (network?.kind === NetworkKind.Cosmos) { + tokenId += network.nameServiceTLD || ""; + } + await queryClient.invalidateQueries( + nsNameInfoQueryKey(selectedWallet?.networkId, tokenId), + ); + setOrganizationData(undefined); + setConfigureVotingFormData(undefined); + setRolesSettingsFormData(undefined); + setMemberSettingsFormData(undefined); + setCurrentStep(0); + setDAOAddress(""); + setLaunchingStep(0); + }; + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx new file mode 100644 index 0000000000..7361b2cfe3 --- /dev/null +++ b/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { Pressable, ScrollView, View, ViewStyle } from "react-native"; + +import trashSVG from "../../../../../assets/icons/trash.svg"; +import walletInputSVG from "../../../../../assets/icons/wallet-input.svg"; +import useSelectedWallet from "../../../../hooks/useSelectedWallet"; + +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { SecondaryButton } from "@/components/buttons/SecondaryButton"; +import { TextInputCustom } from "@/components/inputs/TextInputCustom"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { patternOnlyNumbers, validateAddress } from "@/utils/formRules"; +import { neutral33 } from "@/utils/style/colors"; +import { fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + ROLES_BASED_ORGANIZATION_STEPS, + RolesMemberSettingFormType, +} from "@/utils/types/organizations"; + +interface RolesMembersSettingsSectionProps { + onSubmit: (form: RolesMemberSettingFormType) => void; +} + +export const RolesMembersSettingsSection: React.FC< + RolesMembersSettingsSectionProps +> = ({ onSubmit }) => { + const { handleSubmit, control, resetField, unregister } = + useForm(); + + // this effect put the selected wallet address in the first field only on initial load + const selectedWallet = useSelectedWallet(); + const [initialReset, setInitialReset] = useState(false); + useEffect(() => { + if (initialReset) { + return; + } + if (!selectedWallet?.address) { + return; + } + resetField("members.0.addr", { + defaultValue: selectedWallet?.address, + }); + setInitialReset(true); + }, [selectedWallet?.address, resetField, initialReset]); + + const [addressIndexes, setAddressIndexes] = useState([0]); + + // functions + const removeAddressField = (id: number, index: number) => { + unregister(`members.${index}.addr`); + unregister(`members.${index}.weight`); + unregister(`members.${index}.roles`); + if (addressIndexes.length > 1) { + const copyIndex = [...addressIndexes].filter((i) => i !== id); + setAddressIndexes(copyIndex); + } + }; + + const addAddressField = () => { + setAddressIndexes([...addressIndexes, Math.floor(Math.random() * 200000)]); + }; + + return ( + + + Members + + + {addressIndexes.map((id, index) => ( + + + + name={`members.${index}.addr`} + noBrokenCorners + label="Member Address" + hideLabel={index > 0} + control={control} + rules={{ required: true, validate: validateAddress }} + placeHolder="Account address" + iconSVG={walletInputSVG} + > + removeAddressField(id, index)} + > + + + + + + + + name={`members.${index}.weight`} + noBrokenCorners + label="Weight" + hideLabel={index > 0} + defaultValue="1" + control={control} + rules={{ required: true, pattern: patternOnlyNumbers }} + placeHolder="1" + /> + + + + + name={`members.${index}.roles`} + noBrokenCorners + label="Roles - separate with a comma" + hideLabel={index > 0} + control={control} + placeHolder="administrator, moderator" + /> + + + ))} + + + + + + + + + ); +}; + +const containerCStyle: ViewStyle = { + padding: layout.contentSpacing, + paddingRight: layout.spacing_x2_5, + paddingTop: layout.topContentSpacingWithHeading, +}; + +const leftInputCStyle: ViewStyle = { flex: 4 }; + +const rightInputCStyle: ViewStyle = { flex: 1 }; + +const inputContainerCStyle: ViewStyle = { + flexDirection: "row", + marginBottom: layout.spacing_x2, +}; + +const trashContainerCStyle: ViewStyle = { + height: 16, + width: 16, + justifyContent: "center", + alignItems: "center", + borderRadius: 10, + backgroundColor: "rgba(244, 111, 118, 0.1)", +}; + +const fillCStyle: ViewStyle = { flex: 1 }; + +const footerCStyle: ViewStyle = { + justifyContent: "flex-end", + alignItems: "flex-end", + paddingVertical: layout.spacing_x1_5, + paddingHorizontal: layout.spacing_x2_5, + borderTopWidth: 1, + borderColor: neutral33, +}; diff --git a/packages/screens/Organizations/components/ReviewInformationSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx similarity index 67% rename from packages/screens/Organizations/components/ReviewInformationSection.tsx rename to packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx index 9635a1dac0..cdaccfc245 100644 --- a/packages/screens/Organizations/components/ReviewInformationSection.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx @@ -8,8 +8,8 @@ import { ViewStyle, } from "react-native"; -import { ReviewCollapsable } from "./ReviewCollapsable"; -import { ReviewCollapsableItem } from "./ReviewCollapsableItem"; +import { ReviewCollapsable } from "./../ReviewCollapsable"; +import { ReviewCollapsableItem } from "./../ReviewCollapsableItem"; import { BrandText } from "@/components/BrandText"; import { PrimaryButton } from "@/components/buttons/PrimaryButton"; @@ -24,51 +24,64 @@ import { tinyAddress } from "@/utils/text"; import { ConfigureVotingFormType, CreateDaoFormType, - DaoType, - MemberSettingFormType, + RolesMemberSettingFormType, + RolesSettingFormType, TokenSettingFormType, } from "@/utils/types/organizations"; -interface ReviewInformationSectionProps { +interface RolesReviewInformationSectionProps { organizationData?: CreateDaoFormType; votingSettingData?: ConfigureVotingFormType; tokenSettingData?: TokenSettingFormType; - memberSettingData?: MemberSettingFormType; + memberSettingData?: RolesMemberSettingFormType; + rolesSettingData?: RolesSettingFormType; onSubmit: () => void; } -export const ReviewInformationSection: React.FC< - ReviewInformationSectionProps +export const RolesReviewInformationSection: React.FC< + RolesReviewInformationSectionProps > = ({ organizationData, votingSettingData, tokenSettingData, memberSettingData, + rolesSettingData, onSubmit, }) => { const network = useSelectedNetworkInfo(); - const AddressBalanceValue = useCallback( - ({ address, balance }: { address: string; balance: string }) => ( - - - {tinyAddress(address, 16)} - - - {balance} - - ), - [], - ); - - const AddressWeightValue = useCallback( - ({ address, weight }: { address: string; weight: string }) => ( + const MemberReviewValue = useCallback( + ({ + address, + weight, + roles, + }: { + address: string; + weight: string; + roles: string[]; + }) => ( {tinyAddress(address, 16)} {weight} + + {roles.map((role, index) => ( + + + {role} + + {roles.length !== index + 1 && } + + ))} ), [], @@ -123,14 +136,7 @@ export const ReviewInformationSection: React.FC< value={associateName} /> - + @@ -157,54 +163,44 @@ export const ReviewInformationSection: React.FC< - - {organizationData && - organizationData.structure === DaoType.TOKEN_BASED && ( - + + {rolesSettingData?.roles?.map((role, index) => ( + ( + {role.name} + )} /> - - {tokenSettingData?.tokenHolders.map((holder, index) => ( - - ( - - )} - /> - {tokenSettingData?.tokenHolders.length !== index + 1 && ( - - )} - - ))} - - )} - {organizationData && - organizationData.structure === DaoType.MEMBER_BASED && ( - - {memberSettingData?.members.map((member, index) => ( - - ( - - )} + {rolesSettingData?.roles.length !== index + 1 && ( + + )} + + ))} + + + + + {memberSettingData?.members.map((member, index) => ( + + ( + role.trim())} /> - {tokenSettingData?.tokenHolders.length !== index + 1 && ( - - )} - - ))} - - )} + )} + /> + {tokenSettingData?.tokenHolders.length !== index + 1 && ( + + )} + + ))} + diff --git a/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx new file mode 100644 index 0000000000..74bdbeb867 --- /dev/null +++ b/packages/screens/Organizations/components/RolesOrg/RolesSettingsSection.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Pressable, View } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; + +import trashSVG from "../../../../../assets/icons/trash.svg"; + +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { SecondaryButton } from "@/components/buttons/SecondaryButton"; +import { TextInputCustom } from "@/components/inputs/TextInputCustom"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { neutral33 } from "@/utils/style/colors"; +import { fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { + ROLES_BASED_ORGANIZATION_STEPS, + RolesSettingFormType, +} from "@/utils/types/organizations"; + +interface RolesSettingsSectionProps { + onSubmit: (form: RolesSettingFormType) => void; +} + +export const RolesSettingsSection: React.FC = ({ + onSubmit, +}) => { + const { handleSubmit, control, unregister } = useForm(); + + const [rolesIndexes, setRolesIndexes] = useState([]); + + const removeRoleField = (id: number, index: number) => { + unregister(`roles.${index}.name`); + unregister(`roles.${index}.color`); + if (rolesIndexes.length > 0) { + const copyIndex = [...rolesIndexes].filter((i) => i !== id); + setRolesIndexes(copyIndex); + } + }; + + const addRoleField = () => { + setRolesIndexes([...rolesIndexes, Math.floor(Math.random() * 200000)]); + }; + + return ( + + + Roles + + + {rolesIndexes.map((id, index) => ( + + + + control={control} + noBrokenCorners + name={`roles.${index}.name`} + label="Role name" + placeholder="Role name" + rules={{ required: true }} + placeHolder="Role name" + /> + + + + + control={control} + noBrokenCorners + name={`roles.${index}.color`} + label="Role color" + placeholder="Role color" + placeHolder="Role color" + /> + + + + { + removeRoleField(id, index); + }} + > + + + + + ))} + + + + + + + ); +}; diff --git a/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx b/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx new file mode 100644 index 0000000000..873cc77500 --- /dev/null +++ b/packages/screens/Organizations/components/TokenOrg/TokenDeployerSteps.tsx @@ -0,0 +1,242 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { View } from "react-native"; + +import { TokenReviewInformationSection } from "./TokenReviewInformationSection"; +import { TokenSettingsSection } from "./TokenSettingsSection"; +import { LaunchingOrganizationSection } from "../LaunchingOrganizationSection"; + +import { ConfigureVotingSection } from "@/components/dao/ConfigureVotingSection"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { nsNameInfoQueryKey } from "@/hooks/useNSNameInfo"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { + getNetwork, + getUserId, + mustGetCosmosNetwork, + NetworkKind, +} from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { createDaoTokenBased } from "@/utils/dao"; +import { adenaDeployGnoDAO } from "@/utils/gnodao/deploy"; +import { getDuration, getPercent } from "@/utils/gnodao/helpers"; +import { + ConfigureVotingFormType, + CreateDaoFormType, + LAUNCHING_PROCESS_STEPS, + TOKEN_ORGANIZATION_DEPLOYER_STEPS, + TokenSettingFormType, +} from "@/utils/types/organizations"; + +export const TokenDeployerSteps: React.FC<{ + currentStep: number; + setCurrentStep: (step: number) => void; + launchingStep: number; + setLaunchingStep: (step: number) => void; + organizationData: CreateDaoFormType | undefined; + setOrganizationData: (data: CreateDaoFormType | undefined) => void; +}> = ({ + currentStep, + setCurrentStep, + launchingStep, + setLaunchingStep, + organizationData, + setOrganizationData, +}) => { + const selectedWallet = useSelectedWallet(); + const [daoAddress, setDAOAddress] = useState(""); + const network = getNetwork(selectedWallet?.networkId); + const queryClient = useQueryClient(); + const { setToast } = useFeedbacks(); + const [configureVotingFormData, setConfigureVotingFormData] = + useState(); + const [tokenSettingFormData, setTokenSettingFormData] = + useState(); + + const createDaoContract = async (): Promise => { + try { + switch (network?.kind) { + case NetworkKind.Gno: { + const name = organizationData?.associatedHandle!; + const pkgPath = await adenaDeployGnoDAO( + network.id, + selectedWallet?.address!, + { + name, + maxVotingPeriodSeconds: + parseInt(configureVotingFormData?.days!, 10) * 24 * 60 * 60, + roles: undefined, + initialMembers: [], + thresholdPercent: + configureVotingFormData?.minimumApprovalPercent!, + quorumPercent: configureVotingFormData?.supportPercent!, + displayName: organizationData?.organizationName!, + description: organizationData?.organizationDescription!, + imageURI: organizationData?.imageUrl!, + }, + ); + setLaunchingStep(1); + setDAOAddress(pkgPath); + return true; + } + case NetworkKind.Cosmos: { + if ( + !selectedWallet || + !organizationData || + !configureVotingFormData + ) { + return false; + } + + const networkId = selectedWallet.networkId; + const network = mustGetCosmosNetwork(networkId); + const cwAdminFactoryContractAddress = + network.cwAdminFactoryContractAddress!; + const walletAddress = selectedWallet.address; + const signingClient = await getKeplrSigningCosmWasmClient(networkId); + + if (!tokenSettingFormData) return false; + const createDaoRes = await createDaoTokenBased( + { + client: signingClient, + sender: walletAddress, + contractAddress: cwAdminFactoryContractAddress, + daoPreProposeSingleCodeId: network.daoPreProposeSingleCodeId!, + daoProposalSingleCodeId: network.daoProposalSingleCodeId!, + daoCw20CodeId: network.daoCw20CodeId!, + daoCw20StakeCodeId: network.daoCw20StakeCodeId!, + daoVotingCw20StakedCodeId: network.daoVotingCw20StakedCodeId!, + daoCoreCodeId: network.daoCoreCodeId!, + name: organizationData.organizationName, + description: organizationData.organizationDescription, + tns: organizationData.associatedHandle, + imageUrl: organizationData.imageUrl, + tokenName: tokenSettingFormData.tokenName, + tokenSymbol: tokenSettingFormData.tokenSymbol, + tokenHolders: tokenSettingFormData.tokenHolders.map((item) => { + return { address: item.address, amount: item.balance }; + }), + quorum: getPercent(configureVotingFormData.supportPercent), + threshold: getPercent( + configureVotingFormData.minimumApprovalPercent, + ), + maxVotingPeriod: getDuration( + configureVotingFormData.days, + configureVotingFormData.hours, + configureVotingFormData.minutes, + ), + }, + "auto", + ); + if (createDaoRes) { + return true; + } else { + console.error("Failed to create DAO"); + setToast({ + title: "Failed to create DAO", + mode: "normal", + type: "error", + }); + return false; + } + } + default: { + throw new Error("Network not supported " + selectedWallet?.networkId); + } + } + } catch (err: unknown) { + console.error("Failed to create DAO: ", err); + if (err instanceof Error) { + setToast({ + title: "Failed to create DAO", + message: err.message, + mode: "normal", + type: "error", + }); + } + return false; + } + }; + + // functions + const onSubmitConfigureVoting = (data: ConfigureVotingFormType) => { + setConfigureVotingFormData(data); + setCurrentStep(2); + }; + + const onSubmitTokenSettings = (data: TokenSettingFormType) => { + setTokenSettingFormData(data); + setCurrentStep(3); + }; + + const onStartLaunchingProcess = async () => { + setCurrentStep(4); + if (!(await createDaoContract())) { + setCurrentStep(3); + } + }; + + const resetForm = async () => { + let tokenId = organizationData?.associatedHandle; + const network = getNetwork(selectedWallet?.networkId); + if (network?.kind === NetworkKind.Gno) { + tokenId += ".gno"; + } else if (network?.kind === NetworkKind.Cosmos) { + tokenId += network.nameServiceTLD || ""; + } + await queryClient.invalidateQueries( + nsNameInfoQueryKey(selectedWallet?.networkId, tokenId), + ); + setOrganizationData(undefined); + setConfigureVotingFormData(undefined); + setTokenSettingFormData(undefined); + setCurrentStep(0); + setDAOAddress(""); + setLaunchingStep(0); + }; + + return ( + <> + + + + + + + + + + + + + + ); +}; diff --git a/packages/screens/Organizations/components/TokenOrg/TokenReviewInformationSection.tsx b/packages/screens/Organizations/components/TokenOrg/TokenReviewInformationSection.tsx new file mode 100644 index 0000000000..c9f18d55d8 --- /dev/null +++ b/packages/screens/Organizations/components/TokenOrg/TokenReviewInformationSection.tsx @@ -0,0 +1,228 @@ +import React, { useCallback } from "react"; +import { + Image, + ImageStyle, + ScrollView, + TextStyle, + View, + ViewStyle, +} from "react-native"; + +import { ReviewCollapsable } from "../ReviewCollapsable"; +import { ReviewCollapsableItem } from "../ReviewCollapsableItem"; + +import { BrandText } from "@/components/BrandText"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Separator } from "@/components/separators/Separator"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; +import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork"; +import { NetworkKind } from "@/networks"; +import { neutral00, primaryColor } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { tinyAddress } from "@/utils/text"; +import { + ConfigureVotingFormType, + CreateDaoFormType, + TokenSettingFormType, +} from "@/utils/types/organizations"; + +interface TokenReviewInformationSectionProps { + organizationData?: CreateDaoFormType; + votingSettingData?: ConfigureVotingFormType; + tokenSettingData?: TokenSettingFormType; + onSubmit: () => void; +} + +export const TokenReviewInformationSection: React.FC< + TokenReviewInformationSectionProps +> = ({ organizationData, votingSettingData, tokenSettingData, onSubmit }) => { + const network = useSelectedNetworkInfo(); + + const AddressBalanceValue = useCallback( + ({ address, balance }: { address: string; balance: string }) => ( + + + {tinyAddress(address, 16)} + + + {balance} + + ), + [], + ); + + let associateName = ""; + if (network?.kind === NetworkKind.Gno) { + associateName = "gno.land/r/demo/" + organizationData?.associatedHandle; + } else if (network?.kind === NetworkKind.Cosmos) { + associateName = + organizationData?.associatedHandle || "" + network.nameServiceTLD; + } + + let price = "0"; + const availability = organizationData?.nameAvailability; + if ( + availability && + (availability.availability === "mint" || + availability.availability === "market") + ) { + price = availability.prettyPrice; + } + + return ( + + Review information + + + + ( + + )} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {tokenSettingData?.tokenHolders.map((holder, index) => ( + + ( + + )} + /> + {tokenSettingData?.tokenHolders.length !== index + 1 && ( + + )} + + ))} + + + + + + + Associated Handle: + + + {associateName} + + + + + + + + Price of Organization Deployment: + + + + {price} + + + + + + + ); +}; + +const containerCStyle: ViewStyle = { + padding: layout.contentSpacing, + paddingRight: layout.spacing_x2_5, + paddingTop: layout.topContentSpacingWithHeading, +}; + +const rowCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", +}; + +const rowSBCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", +}; + +const imageCStyle: ImageStyle = { + width: 140, + height: 140, + borderRadius: 12, +}; + +const footerRowInsideCStyle: ViewStyle = { + flexDirection: "row", + alignItems: "center", + height: 24, + flexWrap: "wrap", +}; + +const addressTextCStyle: TextStyle = { + ...fontSemibold14, + padding: layout.spacing_x1, + backgroundColor: neutral00, + borderRadius: 8, +}; + +const fillCStyle: ViewStyle = { flex: 1 }; diff --git a/packages/screens/Organizations/components/TokenSettingsSection.tsx b/packages/screens/Organizations/components/TokenOrg/TokenSettingsSection.tsx similarity index 66% rename from packages/screens/Organizations/components/TokenSettingsSection.tsx rename to packages/screens/Organizations/components/TokenOrg/TokenSettingsSection.tsx index 4e861b6697..dca2178afb 100644 --- a/packages/screens/Organizations/components/TokenSettingsSection.tsx +++ b/packages/screens/Organizations/components/TokenOrg/TokenSettingsSection.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { useForm } from "react-hook-form"; -import { Pressable, ScrollView, StyleSheet, View } from "react-native"; +import { Pressable, ScrollView, View, ViewStyle } from "react-native"; -import trashSVG from "../../../../assets/icons/trash.svg"; -import walletInputSVG from "../../../../assets/icons/wallet-input.svg"; +import trashSVG from "../../../../../assets/icons/trash.svg"; +import walletInputSVG from "../../../../../assets/icons/wallet-input.svg"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; @@ -16,11 +16,11 @@ import { patternOnlyNumbers, validateAddress, } from "@/utils/formRules"; -import { neutral33, neutralA3 } from "@/utils/style/colors"; -import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { neutral33 } from "@/utils/style/colors"; +import { fontSemibold28 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; import { - ORGANIZATION_DEPLOYER_STEPS, + TOKEN_ORGANIZATION_DEPLOYER_STEPS, TokenSettingFormType, } from "@/utils/types/organizations"; @@ -47,14 +47,14 @@ export const TokenSettingsSection: React.FC = ({ }; return ( - - + + Choose your tokens settings below - - + + name="tokenName" noBrokenCorners @@ -65,7 +65,7 @@ export const TokenSettingsSection: React.FC = ({ /> - + name="tokenSymbol" noBrokenCorners @@ -80,8 +80,8 @@ export const TokenSettingsSection: React.FC = ({ {addressIndexes.map((id, index) => ( - - + + name={`tokenHolders.${index}.address`} noBrokenCorners @@ -93,7 +93,7 @@ export const TokenSettingsSection: React.FC = ({ iconSVG={walletInputSVG} > removeAddressField(id)} > @@ -101,7 +101,7 @@ export const TokenSettingsSection: React.FC = ({ - + name={`tokenHolders.${index}.balance`} noBrokenCorners @@ -118,10 +118,10 @@ export const TokenSettingsSection: React.FC = ({ - + @@ -129,41 +129,37 @@ export const TokenSettingsSection: React.FC = ({ ); }; -// FIXME: remove StyleSheet.create -// eslint-disable-next-line no-restricted-syntax -const styles = StyleSheet.create({ - container: { - padding: layout.contentSpacing, - paddingRight: layout.spacing_x2_5, - paddingTop: layout.topContentSpacingWithHeading, - }, - voteText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutralA3, - }, - ]), - leftInput: { flex: 4 }, - rightInput: { flex: 1 }, - inputContainer: { - flexDirection: "row", - marginBottom: layout.spacing_x2, - }, - trashContainer: { - height: 16, - width: 16, - justifyContent: "center", - alignItems: "center", - borderRadius: 10, - backgroundColor: "rgba(244, 111, 118, 0.1)", - }, - fill: { flex: 1 }, - footer: { - justifyContent: "flex-end", - alignItems: "flex-end", - paddingVertical: layout.spacing_x1_5, - paddingHorizontal: layout.spacing_x2_5, - borderTopWidth: 1, - borderColor: neutral33, - }, -}); +const containerCStyle: ViewStyle = { + padding: layout.contentSpacing, + paddingRight: layout.spacing_x2_5, + paddingTop: layout.topContentSpacingWithHeading, +}; + +const leftInputCStyle: ViewStyle = { flex: 4 }; + +const rightInputCStyle: ViewStyle = { flex: 1 }; + +const inputContainerCStyle: ViewStyle = { + flexDirection: "row", + marginBottom: layout.spacing_x2, +}; + +const trashContainerCStyle: ViewStyle = { + height: 16, + width: 16, + justifyContent: "center", + alignItems: "center", + borderRadius: 10, + backgroundColor: "rgba(244, 111, 118, 0.1)", +}; + +const fillCStyle: ViewStyle = { flex: 1 }; + +const footerCStyle: ViewStyle = { + justifyContent: "flex-end", + alignItems: "flex-end", + paddingVertical: layout.spacing_x1_5, + paddingHorizontal: layout.spacing_x2_5, + borderTopWidth: 1, + borderColor: neutral33, +}; diff --git a/packages/utils/gnodao/deploy.ts b/packages/utils/gnodao/deploy.ts index 4ffcdcd66a..385bfdda68 100644 --- a/packages/utils/gnodao/deploy.ts +++ b/packages/utils/gnodao/deploy.ts @@ -4,11 +4,13 @@ import { adenaAddPkg } from "../gno"; interface GnoDAOMember { address: string; weight: number; + roles: string[]; } interface GnoDAOConfig { name: string; maxVotingPeriodSeconds: number; + roles: string[] | undefined; initialMembers: GnoDAOMember[]; thresholdPercent: number; quorumPercent: number; @@ -27,6 +29,7 @@ const generateDAORealmSource = (networkId: string, conf: GnoDAOConfig) => { dao_core "${network.daoCorePkgPath}" dao_interfaces "${network.daoInterfacesPkgPath}" proposal_single "${network.daoProposalSinglePkgPath}" + "${network.rolesGroupPkgPath}" "${network.daoUtilsPkgPath}" "${network.profilePkgPath}" voting_group "${network.votingGroupPkgPath}" @@ -37,6 +40,7 @@ const generateDAORealmSource = (networkId: string, conf: GnoDAOConfig) => { var ( daoCore dao_interfaces.IDAOCore group *voting_group.VotingGroup + roles *dao_roles_group.RolesGroup registered bool ) @@ -46,12 +50,20 @@ func init() { ${conf.initialMembers .map( (member) => - `group.SetMemberPower("${member.address}", ${member.weight});`, + `group.SetMemberPower("${member.address}", ${member.weight})`, ) .join("\n\t")} return group } + rolesModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IRolesModule { + roles = dao_roles_group.NewRolesGroup() + ${(conf.roles ?? []).map((role) => `roles.NewRole("${role}");`).join("\n\t")} + ${conf.initialMembers.map((member) => member.roles.map((role) => `roles.GrantRole("${member.address}", "${role}")`).join("\n\t"))} + return roles + } + + // TODO: consider using factories that return multiple modules and handlers proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ @@ -86,7 +98,7 @@ func init() { }, } - daoCore = dao_core.NewDAOCore(votingModuleFactory, proposalModulesFactories, messageHandlersFactories) + daoCore = dao_core.NewDAOCore(votingModuleFactory, rolesModuleFactory, proposalModulesFactories, messageHandlersFactories) // Register the DAO profile profile.SetStringField(profile.DisplayName, "${conf.displayName}") diff --git a/packages/utils/gnodao/helpers.ts b/packages/utils/gnodao/helpers.ts new file mode 100644 index 0000000000..2fd3f042ce --- /dev/null +++ b/packages/utils/gnodao/helpers.ts @@ -0,0 +1,18 @@ +export const getDuration = ( + days: string | undefined, + hours: string | undefined, + minutes: string | undefined, +): number => { + const num_days = !days ? 0 : parseInt(days, 10); + const num_hours = !hours ? 0 : parseInt(hours, 10); + const num_minutes = !minutes ? 0 : parseInt(minutes, 10); + return num_days * 3600 * 24 + num_hours * 3600 + num_minutes * 60; +}; + +export const getPercent = (num: number | undefined): string => { + let ret_num: number; + ret_num = num === undefined ? 0 : num; + ret_num = ret_num < 0 ? 0 : ret_num; + ret_num = ret_num > 100 ? 100 : ret_num; + return (ret_num / 100).toFixed(2); +}; diff --git a/packages/utils/style/fonts.ts b/packages/utils/style/fonts.ts index 1b7bdb8784..52e9f883e0 100644 --- a/packages/utils/style/fonts.ts +++ b/packages/utils/style/fonts.ts @@ -144,13 +144,6 @@ export const fontSemibold9: TextStyle = { fontFamily: "Exo_600SemiBold", fontWeight: "600", }; -export const fontSemibold8: TextStyle = { - fontSize: 8, - lineHeight: 10, - fontFamily: "Exo_600SemiBold", - fontWeight: "600", - letterSpacing: -(8 * 0.04), -}; export const fontMedium48: TextStyle = { fontSize: 48, diff --git a/packages/utils/types/organizations.ts b/packages/utils/types/organizations.ts index 2de837771d..66673bdeaa 100644 --- a/packages/utils/types/organizations.ts +++ b/packages/utils/types/organizations.ts @@ -3,8 +3,11 @@ import { NSAvailability } from "./tns"; export enum DaoType { MEMBER_BASED = 0, TOKEN_BASED = 1, + ROLES_BASED = 2, } +// SHARED BETWEEN ALL ORGANIZATION TYPES + export type CreateDaoFormType = { organizationName: string; associatedHandle: string; @@ -14,6 +17,8 @@ export type CreateDaoFormType = { nameAvailability: NSAvailability; }; +// GENERAL AND SHAREABLE FORM TYPES + export type ConfigureVotingFormType = { supportPercent: number; minimumApprovalPercent: number; @@ -22,30 +27,61 @@ export type ConfigureVotingFormType = { minutes: string; }; -export type TokenSettingFormType = { - tokenName: string; - tokenSymbol: string; - tokenHolders: { address: string; balance: string }[]; +export type LaunchingProcessStepType = { + title: string; + completeText: string; + isComplete?: boolean; }; -export type MemberSettingFormType = { +// MEMBERSHIP BASED ORGANIZATION FORM TYPES + +export type MembershipMemberSettingFormType = { members: { addr: string; weight: string }[]; }; -export type LaunchingProcessStepType = { - title: string; - completeText: string; - isComplete?: boolean; +// ROLES BASED ORGANIZATION FORM TYPES + +export type RolesSettingFormType = { + roles: { name: string; color: string }[]; +}; + +export type RolesMemberSettingFormType = { + members: { addr: string; weight: string; roles: string | undefined }[]; }; -export const ORGANIZATION_DEPLOYER_STEPS = [ +// TOKEN BASED ORGANIZATION FORM TYPES + +export type TokenSettingFormType = { + tokenName: string; + tokenSymbol: string; + tokenHolders: { address: string; balance: string }[]; +}; + +export const ROLES_BASED_ORGANIZATION_STEPS = [ "Create a DAO", "Configure voting", + "Define Roles & Permissions", "Set tokens or members", "Review information", "Launch organization", ]; +export const MEMBERSHIP_ORGANIZATION_DEPLOYER_STEPS = [ + "Create a DAO", + "Configure voting", + "Set members", + "Review information", + "Launch organization", +]; + +export const TOKEN_ORGANIZATION_DEPLOYER_STEPS = [ + "Create a DAO", + "Configure voting", + "Set tokens", + "Review information", + "Launch organization", +]; + export const LAUNCHING_PROCESS_STEPS: LaunchingProcessStepType[] = [ { title: "Create organization", completeText: "Transaction finalized" }, ];