diff --git a/src/screens/Profile/Vouches/Create/index.tsx b/src/screens/Profile/Vouches/Create/index.tsx
index a7da764a32..1f193f074f 100644
--- a/src/screens/Profile/Vouches/Create/index.tsx
+++ b/src/screens/Profile/Vouches/Create/index.tsx
@@ -1,9 +1,21 @@
-import {Trans} from '@lingui/macro'
+import React from 'react'
+import {View} from 'react-native'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {ZodError} from 'zod'
import * as Layout from '#/components/Layout'
-import {Text} from '#/components/Typography'
+import {atoms as a, useGutters} from '#/alf'
+import {Button, ButtonText, ButtonIcon} from '#/components/Button'
+import * as TextField from '#/components/forms/TextField'
+import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch'
+import {Loader} from '#/components/Loader'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {Admonition} from '#/components/Admonition'
export function Screen() {
+ const baseGutters = useGutters(['base'])
+
return (
@@ -17,8 +29,77 @@ export function Screen() {
- Create
+
+
+
)
}
+
+function Form() {
+ const {_} = useLingui()
+ const [subject, setSubject] = React.useState('')
+ const [relationship, setRelationship] = React.useState('')
+ const [errors, setErrors] = React.useState([])
+
+ const {mutateAsync: createVouch, isPending} = useCreateVouch()
+
+ const onSubmit = async () => {
+ setErrors([])
+ try {
+ await createVouch({subject, relationship})
+ } catch (e: any) {
+ if (e instanceof ZodError) {
+ setErrors(e.errors.map(err => err.message))
+ } else {
+ setErrors([e.message])
+ }
+ }
+ }
+
+ return (
+
+
+
+ User DID
+
+
+
+
+
+ User DID
+
+
+
+
+
+
+
+
+ {!!errors.length && (
+
+ {errors.map((error, i) => (
+
+ {' '}
+ {error}{' '}
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/src/screens/Profile/Vouches/Issued/index.tsx b/src/screens/Profile/Vouches/Issued/index.tsx
index c45d94abdd..5f30b14298 100644
--- a/src/screens/Profile/Vouches/Issued/index.tsx
+++ b/src/screens/Profile/Vouches/Issued/index.tsx
@@ -1,9 +1,22 @@
-import {Trans} from '@lingui/macro'
+import React from 'react'
+import {View} from 'react-native'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {ZodError} from 'zod'
import * as Layout from '#/components/Layout'
+import {atoms as a, useGutters} from '#/alf'
+import {Button, ButtonText, ButtonIcon} from '#/components/Button'
+import * as TextField from '#/components/forms/TextField'
+import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch'
+import {Loader} from '#/components/Loader'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {Admonition} from '#/components/Admonition'
+import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued'
import {Text} from '#/components/Typography'
export function Screen() {
+ const baseGutters = useGutters(['base'])
return (
@@ -17,8 +30,38 @@ export function Screen() {
- Create
+
+
+
)
}
+
+export function Inner() {
+ const {data: vouches, isLoading, error} = useVouchesIssued()
+
+ return (
+
+
+ {isLoading ? (
+
+ ) : error || !vouches ? (
+ <>>
+ ) : vouches.length ? (
+ <>
+ {vouches.map(v => (
+
+ {v.record.subject}
+
+ ))}
+ >
+ ) : (
+ <>
+ No vouches
+ >
+ )}
+
+
+ )
+}
diff --git a/src/screens/Profile/Vouches/index.tsx b/src/screens/Profile/Vouches/index.tsx
index f52f0f65fc..7323b61b3d 100644
--- a/src/screens/Profile/Vouches/index.tsx
+++ b/src/screens/Profile/Vouches/index.tsx
@@ -1,9 +1,26 @@
-import {Trans} from '@lingui/macro'
+import {View, ScrollView} from 'react-native'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {Loader} from '#/components/Loader'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
+import {atoms as a, useTheme, useGutters} from '#/alf'
+import {Link} from '#/components/Link'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
+import {useSession} from '#/state/session'
+import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued'
+import {Button, ButtonText, ButtonIcon} from '#/components/Button'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {Divider} from '#/components/Divider'
export function Screen() {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const baseGutters = useGutters(['base'])
+ const compactGutters = useGutters(['compact'])
+
return (
@@ -17,8 +34,111 @@ export function Screen() {
- Vouches
+
+
+
+ {({hovered}) => (
+
+
+ Create vouch
+
+
+
+
+ )}
+
+
+
+
+
)
}
+
+export function VouchesIssued() {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const {data: vouches, isLoading, error} = useVouchesIssued()
+ const baseGutters = useGutters([0, 'base'])
+
+ return (
+
+
+
+ Vouches Issued
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : error || !vouches ? (
+ <>
+ {error ? (
+ {error.toString()}
+ ) : (
+
+ Somthing went wrong
+
+ )}
+ >
+ ) : vouches.length ? (
+
+ {vouches.map(v => (
+
+
+
+
+ @{v.subject?.handle}
+ {v.record.relationship}
+
+
+
+
+ {v.accept ? 'Accepted' : 'Pending'}
+ {v.record.createdAt}
+
+
+ ))}
+
+ ) : (
+ <>
+ No vouches
+ >
+ )}
+
+
+ )
+}
diff --git a/src/state/queries/vouches/useCreateVouch.ts b/src/state/queries/vouches/useCreateVouch.ts
new file mode 100644
index 0000000000..0c871cf5b0
--- /dev/null
+++ b/src/state/queries/vouches/useCreateVouch.ts
@@ -0,0 +1,31 @@
+import {AppBskyGraphVouch} from '@atproto/api'
+import {useMutation} from '@tanstack/react-query'
+
+import {useSession, useAgent} from '#/state/session'
+import {useVouchRecordSchema} from '#/state/queries/vouches/util'
+
+export type CreateVouchProps = {
+ subject: string
+ relationship: AppBskyGraphVouch.Record['relationship']
+}
+
+export function useCreateVouch() {
+ const {currentAccount} = useSession()
+ const agent = useAgent()
+ const vouchRecordSchema = useVouchRecordSchema()
+
+ return useMutation({
+ mutationFn: async (props: CreateVouchProps) => {
+ const record: AppBskyGraphVouch.Record = {
+ subject: props.subject,
+ relationship: props.relationship,
+ createdAt: new Date().toISOString(),
+ }
+ vouchRecordSchema.parse(record)
+ return agent.app.bsky.graph.vouch.create(
+ { repo: currentAccount!.did },
+ record,
+ )
+ }
+ })
+}
diff --git a/src/state/queries/vouches/useVouchesIssued.ts b/src/state/queries/vouches/useVouchesIssued.ts
new file mode 100644
index 0000000000..331a17b4ab
--- /dev/null
+++ b/src/state/queries/vouches/useVouchesIssued.ts
@@ -0,0 +1,21 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {useSession, useAgent} from '#/state/session'
+
+export const vouchesIssuedQueryKey = ['vouches-issued']
+
+export function useVouchesIssued() {
+ const {currentAccount} = useSession()
+ const agent = useAgent()
+
+ return useQuery({
+ queryKey: vouchesIssuedQueryKey,
+ queryFn: async () => {
+ const {data} = await agent.app.bsky.graph.getVouchesGiven({
+ actor: currentAccount!.did,
+ includeUnaccepted: true,
+ })
+ return data.vouches
+ }
+ })
+}
diff --git a/src/state/queries/vouches/util.ts b/src/state/queries/vouches/util.ts
new file mode 100644
index 0000000000..9394858d3c
--- /dev/null
+++ b/src/state/queries/vouches/util.ts
@@ -0,0 +1,16 @@
+import {useMemo} from 'react'
+import zod from 'zod'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+export function useVouchRecordSchema() {
+ const {_} = useLingui()
+
+ return useMemo(() => {
+ return zod.object({
+ subject: zod.string().startsWith('did:', { message: _(msg`Must be a valid DID`) }),
+ relationship: zod.enum(['verifiedBy', 'employeeOf']),
+ createdAt: zod.string().datetime(),
+ })
+ }, [_])
+}