Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4232 signer modal component #4362

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/ui/src/app/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { MemberModalCall, MemberProfile } from '@/memberships/components/MemberP
import { useMyMemberships } from '@/memberships/hooks/useMyMemberships'
import { BuyMembershipModal, BuyMembershipModalCall } from '@/memberships/modals/BuyMembershipModal'
import { DisconnectWalletModal, DisconnectWalletModalCall } from '@/memberships/modals/DisconnectWalletModal'
import { EmailSubscriptionModal, EmailSubscriptionModalCall } from '@/memberships/modals/EmailSubscriptionModal'
import { InviteMemberModal } from '@/memberships/modals/InviteMemberModal'
import { InviteMemberModalCall } from '@/memberships/modals/InviteMemberModal/types'
import { SignOutModal } from '@/memberships/modals/SignOutModal/SignOutModal'
Expand Down Expand Up @@ -122,6 +123,7 @@ export type ModalNames =
| ModalName<ReportContentModalCall>
| ModalName<PostReplyModalCall>
| ModalName<InviteMemberModalCall>
| ModalName<EmailSubscriptionModalCall>

const modals: Record<ModalNames, ReactElement> = {
Member: <MemberProfile />,
Expand Down Expand Up @@ -171,6 +173,7 @@ const modals: Record<ModalNames, ReactElement> = {
UpdateMembershipModal: <UpdateMembershipModal />,
ReportContentModal: <ReportContentModal />,
PostReplyModal: <PostReplyModal />,
EmailSubscriptionModal: <EmailSubscriptionModal />,
}

const GUEST_ACCESSIBLE_MODALS: ModalNames[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import React from 'react'
import React, { useEffect } from 'react'
import styled from 'styled-components'

import { useMyAccounts } from '@/accounts/hooks/useMyAccounts'
import { ButtonPrimary } from '@/common/components/buttons'
import { ArrowDownExpandedIcon, Icon } from '@/common/components/icons'
import { BorderRad, Colors, Transitions } from '@/common/constants'
import { useLocalStorage } from '@/common/hooks/useLocalStorage'
import { useModal } from '@/common/hooks/useModal'

import { MemberDarkHover, MemberInfo, MembershipsCount } from '..'
import { useMyMemberships } from '../../hooks/useMyMemberships'
import { EmailSubscriptionModalCall } from '../../modals/EmailSubscriptionModal'
import { SwitchMemberModalCall } from '../../modals/SwitchMemberModal'
import { AddMembershipButton } from '../AddMembershipButton'

export const CurrentMember = () => {
const { wallet } = useMyAccounts()
const { members, hasMembers, active } = useMyMemberships()
const { showModal } = useModal()
const [memberEmail] = useLocalStorage('memberEmail')

useEffect(() => {
const showSubscriptionModal = typeof memberEmail !== 'string' && active
thesan marked this conversation as resolved.
Show resolved Hide resolved
if (showSubscriptionModal) {
showModal<EmailSubscriptionModalCall>({
modal: 'EmailSubscriptionModal',
data: { member: active },
})
}
}, [active])

if (!wallet) {
return (
<MembershipButtonsWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import styled from 'styled-components'
import * as Yup from 'yup'

import { ButtonGhost } from '@/common/components/buttons'
import { InputComponent, InputText } from '@/common/components/forms'
import { Info } from '@/common/components/Info'
import { ModalHeader, ModalTransactionFooter, Row, Modal, ModalBody } from '@/common/components/Modal'
import { TextMedium } from '@/common/components/typography'
import { useLocalStorage } from '@/common/hooks/useLocalStorage'
import { useYupValidationResolver } from '@/common/utils/validation'

import { Member } from '../../types'

import { EmailSubscriptionForm } from './types'

interface Props {
onClose: () => void
onSubmit: (params: EmailSubscriptionForm) => void
member: Member
}

const EmailSubscriptionSchema = Yup.object().shape({
email: Yup.string().email().required('This field is required.'),
})

export const EmailSubscriptionFormModal = ({ onClose, onSubmit, member }: Props) => {
const [, setEmail] = useLocalStorage('memberEmail')
thesan marked this conversation as resolved.
Show resolved Hide resolved

const form = useForm({
resolver: useYupValidationResolver<EmailSubscriptionForm>(EmailSubscriptionSchema),
mode: 'onChange',
})

const onCancelClick = () => {
setEmail('')
thesan marked this conversation as resolved.
Show resolved Hide resolved
onClose()
}

const onSubmitClick = () => {
onSubmit({
email: form.getValues('email'),
id: member.id,
name: member.name,
})
}
thesan marked this conversation as resolved.
Show resolved Hide resolved

const isValid = !form.getValues('email') || form.getFieldState('email').invalid

return (
<Modal modalSize="m" modalHeight="m" onClose={onCancelClick}>
<ModalHeader onClick={onCancelClick} title="Sign up to email notifications" />
<ModalBody>
<FormProvider {...form}>
<Row>
<InputComponent
id="email"
label="Email"
required
inputSize="l"
tooltipText="Add your email address here to receive notifications. It can be the same or different to the one added to the membership profile."
>
<InputText id="email" placeholder="Add email for notifications here" required name="email" />
</InputComponent>
</Row>
</FormProvider>
<Row>
<Info title="Your email will never be shared and does not go on chain">
<TextMedium light>
We use your email only to send you important notifications. You can change this email, opt out from
notifications and customize what kind of notifications you receive anytime in settings.
</TextMedium>
</Info>
</Row>
</ModalBody>
<StyledFooter
next={{
disabled: isValid,
label: 'Sign and Authorize Email',
onClick: onSubmitClick,
}}
>
<ButtonGhost size="medium" onClick={onCancelClick}>
cancel
thesan marked this conversation as resolved.
Show resolved Hide resolved
</ButtonGhost>
</StyledFooter>
</Modal>
)
}

const StyledFooter = styled(ModalTransactionFooter)`
grid-column-gap: 20px;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react'

import { useApi } from '@/api/hooks/useApi'
import { TextMedium } from '@/common/components/typography'
import { useMachine } from '@/common/hooks/useMachine'
import { useModal } from '@/common/hooks/useModal'
import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal'
import { EmailSubscriptionModalCall } from '@/memberships/modals/EmailSubscriptionModal/index'

import { EmailSubscriptionFormModal } from './EmaiSubscriptionFormModal'
import { EmailSubscriptionMachine } from './machine'
import { EmailSubscriptionForm } from './types'
import { createBatch } from './utils'

export const EmailSubscriptionModal = () => {
const { api } = useApi()
const {
hideModal,
modalData: { member },
} = useModal<EmailSubscriptionModalCall>()
const [state, send] = useMachine(EmailSubscriptionMachine)

Copy link
Member

@thesan thesan May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the sign modal can be triggered by simply calling:

const signature = await api.sign(member.controllerAccount, `${member.id}:${timestamp}`)

Keep in mind the signup mutation is:

signup(memberId: String! signature: String! timestamp: BigInt! name: String! email: String): String

(As documented in the backend readme). So the signature and timestamp have to be sent too.

if (state.matches('prepare')) {
return (
<EmailSubscriptionFormModal
onClose={hideModal}
onSubmit={(params: EmailSubscriptionForm) => send('DONE', { form: params })}
member={member}
/>
)
}
Comment on lines +34 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step doesn't exist anymore


if (state.matches('transaction')) {
return (
<SignTransactionModal
buttonText="Sign and subscribe"
transaction={createBatch(state.context.form, api)}
signer={member.controllerAccount}
service={state.children.transaction}
>
<TextMedium>You subscribed to emal.</TextMedium>
</SignTransactionModal>
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too the logic shoiuld be similar to @common/modals/OnBoardingModal/OnBoardingModal:

Suggested change
return (
<SignTransactionModal
buttonText="Sign and subscribe"
transaction={createBatch(state.context.form, api)}
signer={member.controllerAccount}
service={state.children.transaction}
>
<TextMedium>You subscribed to emal.</TextMedium>
</SignTransactionModal>
)
return (
<WaitModal
onClose={hideModal}
title="Pending transaction"
description="Registering email address..."
/>
)

}

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ModalWithDataCall } from '@/common/providers/modal/types'
import { Member } from '@/memberships/types'

export * from './EmailSubscriptionModal'

export type EmailSubscriptionModalCall = ModalWithDataCall<
'EmailSubscriptionModal',
{
member: Member
}
>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { EventRecord } from '@polkadot/types/interfaces/system'
import { assign, createMachine } from 'xstate'

import { transactionModalFinalStatusesFactory } from '@/common/modals/utils'
import {
isTransactionCanceled,
isTransactionError,
isTransactionSuccess,
transactionMachine,
} from '@/common/model/machines'
import { EmptyObject } from '@/common/types'

import { EmailSubscriptionForm } from './types'

interface EmailSubscriptionContext {
form?: EmailSubscriptionForm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context should include the email, the timestamp, and the signature

}

interface TransactionContext {
transactionEvents?: EventRecord[]
}

type Context = EmailSubscriptionContext & TransactionContext

type EmailSubscriptionState =
| { value: 'prepare'; context: EmptyObject }
| { value: 'transaction'; context: Required<EmailSubscriptionContext> }
| { value: 'success'; context: Required<EmailSubscriptionContext> }
| { value: 'error'; context: Required<Context> }

export type EmailSubscriptionEvent =
| { type: 'PASS' }
| { type: 'FAIL' }
| { type: 'DONE'; form: EmailSubscriptionForm }
| { type: 'SUCCESS' }
| { type: 'ERROR' }

export const EmailSubscriptionMachine = createMachine<Context, EmailSubscriptionEvent, EmailSubscriptionState>({
initial: 'prepare',
states: {
prepare: {
on: {
DONE: {
target: 'transaction',
actions: assign({ form: (_, event) => event.form }),
},
},
},
transaction: {
invoke: {
id: 'transaction',
src: transactionMachine,
onDone: [
{
target: 'success',
cond: isTransactionSuccess,
},
{
target: 'error',
cond: isTransactionError,
},
{
target: 'canceled',
cond: isTransactionCanceled,
},
],
},
},
thesan marked this conversation as resolved.
Show resolved Hide resolved
...transactionModalFinalStatusesFactory({
metaMessages: {
error: 'There was a problem email subscription.',
},
}),
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface EmailSubscriptionForm {
id: string
name?: string
email: string
}
20 changes: 20 additions & 0 deletions packages/ui/src/memberships/modals/EmailSubscriptionModal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SubmittableExtrinsic } from '@polkadot/api/types'

import { Api } from '@/api'

import { EmailSubscriptionForm } from './types'

export function createBatch(transactionParams: EmailSubscriptionForm, api: Api | undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "transaction" will actually just be a GQL mutation to the BE endpoint (because these membership data won't be stored on chain for confidentiality reasons), so this function won't be useful AFAICS.

const transactions: SubmittableExtrinsic<'rxjs'>[] = []

if (!api || !transactionParams.email) {
return
}

if (transactionParams.email) {
// const emailSubscription = api.tx.members.emailSubscription(transactionParams)
// transactions.push(emailSubscription)
}

return api.tx.utility.batch(transactions)
}
1 change: 1 addition & 0 deletions packages/ui/src/memberships/types/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface Member {
isCouncilMember: boolean
createdAt: string
boundAccountsEvents?: BoundAccountEvent[]
email?: string
IlyaSmiyukha marked this conversation as resolved.
Show resolved Hide resolved
}

export type GenesisEntry = {
Expand Down