mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-09 12:27:59 -05:00
Merge pull request #105 from MAGICGrants/mailing-address-save
Mailing address save
This commit is contained in:
@@ -9,7 +9,15 @@ import { z } from 'zod'
|
||||
import { StrapiPerkPopulated } from '../server/types'
|
||||
import { env } from '../env.mjs'
|
||||
import { Button } from './ui/button'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Input } from './ui/input'
|
||||
import { toast } from './ui/use-toast'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
@@ -36,6 +44,7 @@ import {
|
||||
CarouselPrevious,
|
||||
} from './ui/carousel'
|
||||
import CustomLink from './CustomLink'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
|
||||
type Props = { perk: StrapiPerkPopulated; balance: number; close: () => void }
|
||||
|
||||
@@ -43,7 +52,6 @@ const pointFormat = Intl.NumberFormat('en', { notation: 'standard', compactDispl
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
_shippingStateOptionsLength: z.number(),
|
||||
shippingAddressLine1: z.string().min(1),
|
||||
shippingAddressLine2: z.string(),
|
||||
shippingCity: z.string().min(1),
|
||||
@@ -56,6 +64,8 @@ const schema = z
|
||||
.regex(/^\+?\d{6,15}$/, 'Invalid phone number.'),
|
||||
shippingTaxNumber: z.string(),
|
||||
printfulSyncVariantId: z.string().optional(),
|
||||
_shippingStateOptionsLength: z.number(),
|
||||
_useAccountMailingAddress: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const cpfRegex =
|
||||
@@ -100,6 +110,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
const router = useRouter()
|
||||
const fundSlug = useFundSlug()
|
||||
const getCountriesQuery = trpc.perk.getCountries.useQuery()
|
||||
const getUserAttributesQuery = trpc.account.getUserAttributes.useQuery()
|
||||
const purchasePerkMutation = trpc.perk.purchasePerk.useMutation()
|
||||
const estimatePrintfulOrderCosts = trpc.perk.estimatePrintfulOrderCosts.useMutation()
|
||||
|
||||
@@ -139,6 +150,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
const shippingCountry = form.watch('shippingCountry')
|
||||
const shippingState = form.watch('shippingState')
|
||||
const printfulSyncVariantId = form.watch('printfulSyncVariantId')
|
||||
const useAccountMailingAddress = form.watch('_useAccountMailingAddress')
|
||||
|
||||
const shippingStateOptions = useMemo(() => {
|
||||
const selectedCountry = (getCountriesQuery.data || []).find(
|
||||
@@ -163,6 +175,26 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
form.setValue('_shippingStateOptionsLength', shippingStateOptions.length)
|
||||
}, [shippingStateOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!getUserAttributesQuery.data) return
|
||||
|
||||
if (useAccountMailingAddress) {
|
||||
form.setValue('shippingAddressLine1', getUserAttributesQuery.data.addressLine1)
|
||||
form.setValue('shippingAddressLine2', getUserAttributesQuery.data.addressLine2)
|
||||
form.setValue('shippingCountry', getUserAttributesQuery.data.addressCountry)
|
||||
form.setValue('shippingCity', getUserAttributesQuery.data.addressCity)
|
||||
form.setValue('shippingZip', getUserAttributesQuery.data.addressZip)
|
||||
setTimeout(() => form.setValue('shippingState', getUserAttributesQuery.data.addressState), 20)
|
||||
} else {
|
||||
form.setValue('shippingAddressLine1', '')
|
||||
form.setValue('shippingAddressLine2', '')
|
||||
form.setValue('shippingCountry', '')
|
||||
form.setValue('shippingState', '')
|
||||
form.setValue('shippingCity', '')
|
||||
form.setValue('shippingZip', '')
|
||||
}
|
||||
}, [useAccountMailingAddress])
|
||||
|
||||
async function onSubmit(data: PerkPurchaseInputs) {
|
||||
if (!fundSlug) return
|
||||
|
||||
@@ -342,6 +374,23 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
|
||||
{perk.needsShippingAddress && !getPrintfulProductVariantsQuery.data && <Spinner />}
|
||||
|
||||
{getUserAttributesQuery.data?.addressLine1 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="_useAccountMailingAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 flex flex-col items-start leading-none">
|
||||
<FormLabel>Use saved mailing address</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shippingAddressLine1"
|
||||
@@ -349,7 +398,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Address line 1 *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -363,7 +412,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Address line 2</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -381,23 +430,26 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
open={countrySelectOpen}
|
||||
onOpenChange={(open) => setCountrySelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<FormControl>
|
||||
<Select
|
||||
open={countrySelectOpen}
|
||||
onValueChange={() => setCountrySelectOpen(false)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
(getCountriesQuery.data || []).find(
|
||||
(country) => country.code === shippingCountry
|
||||
)?.name || ''
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select
|
||||
open={countrySelectOpen}
|
||||
onValueChange={() => setCountrySelectOpen(false)}
|
||||
disabled={useAccountMailingAddress}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
(getCountriesQuery.data || []).find(
|
||||
(country) => country.code === shippingCountry
|
||||
)?.name || ''
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
@@ -447,20 +499,22 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
open={stateSelectOpen}
|
||||
onOpenChange={(open) => setStateSelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<FormControl>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
shippingStateOptions.find(
|
||||
(state) => state.value === shippingState
|
||||
)?.label
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select disabled={useAccountMailingAddress}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
shippingStateOptions.find(
|
||||
(state) => state.value === shippingState
|
||||
)?.label
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
@@ -506,7 +560,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>City *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -520,7 +574,7 @@ function PerkPurchaseFormModal({ perk, balance, close }: Props) {
|
||||
<FormItem>
|
||||
<FormLabel>Postal code *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={useAccountMailingAddress} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
@@ -25,6 +27,18 @@ import { useToast } from './ui/use-toast'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
import Spinner from './Spinner'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
|
||||
import { Select, SelectTrigger, SelectValue } from './ui/select'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from './ui/command'
|
||||
import { cn } from '../utils/cn'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -38,14 +52,70 @@ const schema = z
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-záéíóúÁÉÍÓÚñÑçÇ]+$/, 'Use alphabetic characters only.'),
|
||||
company: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
_addMailingAddress: z.boolean(),
|
||||
address: z
|
||||
.object({
|
||||
addressLine1: z.string(),
|
||||
addressLine2: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string(),
|
||||
country: z.string(),
|
||||
zip: z.string(),
|
||||
_addressStateOptionsLength: z.number(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.state && data._addressStateOptionsLength) {
|
||||
ctx.addIssue({
|
||||
path: ['shippingState'],
|
||||
code: 'custom',
|
||||
message: 'State is required.',
|
||||
})
|
||||
}
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match.',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data._addMailingAddress) {
|
||||
if (!data.address.addressLine1) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.addressLine1'],
|
||||
code: 'custom',
|
||||
message: 'Address line 1 is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.country) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.country'],
|
||||
code: 'custom',
|
||||
message: 'Country is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.city) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.city'],
|
||||
code: 'custom',
|
||||
message: 'City is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.zip) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.zip'],
|
||||
code: 'custom',
|
||||
message: 'Postal code is required.',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
type RegisterFormInputs = z.infer<typeof schema>
|
||||
|
||||
@@ -53,10 +123,77 @@ type Props = { close: () => void; openLoginModal: () => void }
|
||||
|
||||
function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
const { toast } = useToast()
|
||||
const form = useForm<RegisterFormInputs>({ resolver: zodResolver(schema) })
|
||||
const fundSlug = useFundSlug()
|
||||
const getCountriesQuery = trpc.perk.getCountries.useQuery()
|
||||
const registerMutation = trpc.auth.register.useMutation()
|
||||
|
||||
const form = useForm<RegisterFormInputs>({
|
||||
resolver: zodResolver(schema),
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
company: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
_addMailingAddress: false,
|
||||
address: {
|
||||
_addressStateOptionsLength: 0,
|
||||
addressLine1: '',
|
||||
addressLine2: '',
|
||||
city: '',
|
||||
country: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const addressCountryOptions = (getCountriesQuery.data || []).map((country) => ({
|
||||
label: country.name,
|
||||
value: country.code,
|
||||
}))
|
||||
|
||||
const addressCountry = form.watch('address.country')
|
||||
const addressState = form.watch('address.state')
|
||||
const addMailingAddress = form.watch('_addMailingAddress')
|
||||
|
||||
const addressStateOptions = useMemo(() => {
|
||||
const selectedCountry = (getCountriesQuery.data || []).find(
|
||||
(country) => country.code === addressCountry
|
||||
)
|
||||
|
||||
const stateOptions =
|
||||
selectedCountry?.states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.code,
|
||||
})) || []
|
||||
|
||||
return stateOptions
|
||||
}, [addressCountry])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('address.state', '')
|
||||
}, [addressCountry])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('address._addressStateOptionsLength', addressStateOptions.length)
|
||||
}, [addressStateOptions])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('address._addressStateOptionsLength', 0)
|
||||
form.setValue('address.addressLine1', '')
|
||||
form.setValue('address.addressLine2', '')
|
||||
form.setValue('address.city', '')
|
||||
form.setValue('address.country', '')
|
||||
form.setValue('address.state', '')
|
||||
form.setValue('address.zip', '')
|
||||
}, [addMailingAddress])
|
||||
|
||||
const [countrySelectOpen, setCountrySelectOpen] = useState(false)
|
||||
const [stateSelectOpen, setStateSelectOpen] = useState(false)
|
||||
|
||||
async function onSubmit(data: RegisterFormInputs) {
|
||||
if (!fundSlug) return
|
||||
|
||||
@@ -97,7 +234,7 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem className="grow">
|
||||
<FormLabel>First name</FormLabel>
|
||||
<FormLabel>First name *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
@@ -111,7 +248,7 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem className="grow">
|
||||
<FormLabel>Last name</FormLabel>
|
||||
<FormLabel>Last name *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
@@ -121,12 +258,26 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="company"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Company (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="johndoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
@@ -140,7 +291,7 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>Password *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
@@ -154,7 +305,7 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm password</FormLabel>
|
||||
<FormLabel>Confirm password *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
@@ -163,6 +314,217 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="_addMailingAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 flex flex-col items-start leading-none">
|
||||
<FormLabel>Add mailing address</FormLabel>
|
||||
<FormDescription>
|
||||
You can also manage your mailing address in the account settings.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{addMailingAddress && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.addressLine1"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address line 1 *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.addressLine2"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address line 2</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.country"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Country *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={countrySelectOpen}
|
||||
onOpenChange={(open) => setCountrySelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select
|
||||
open={countrySelectOpen}
|
||||
onValueChange={() => setCountrySelectOpen(false)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
(getCountriesQuery.data || []).find(
|
||||
(country) => country.code === addressCountry
|
||||
)?.name || ''
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search country..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No country found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{addressCountryOptions.map((country) => (
|
||||
<CommandItem
|
||||
value={country.label}
|
||||
key={country.value}
|
||||
onSelect={() => (
|
||||
form.setValue('address.country', country.value, {
|
||||
shouldValidate: true,
|
||||
}),
|
||||
setCountrySelectOpen(false)
|
||||
)}
|
||||
>
|
||||
{country.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
country.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!!addressStateOptions.length && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.state"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>State *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={stateSelectOpen}
|
||||
onOpenChange={(open) => setStateSelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
addressStateOptions.find(
|
||||
(state) => state.value === addressState
|
||||
)?.label
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search state..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No state found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{addressStateOptions.map((state) => (
|
||||
<CommandItem
|
||||
value={state.label}
|
||||
key={state.value}
|
||||
onSelect={() => (
|
||||
form.setValue('address.state', state.value, {
|
||||
shouldValidate: true,
|
||||
}),
|
||||
setStateSelectOpen(false)
|
||||
)}
|
||||
>
|
||||
{state.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
state.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>City *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address.zip"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postal code *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
25
components/ui/checkbox.tsx
Normal file
25
components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from '@radix-ui/react-icons'
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
70
package-lock.json
generated
70
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@keycloak/keycloak-admin-client": "^24.0.5",
|
||||
"@prisma/client": "^5.15.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
@@ -3207,6 +3208,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz",
|
||||
"integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@keycloak/keycloak-admin-client": "^24.0.5",
|
||||
"@prisma/client": "^5.15.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import Head from 'next/head'
|
||||
@@ -19,6 +21,21 @@ import { trpc } from '../../../utils/trpc'
|
||||
import { useFundSlug } from '../../../utils/use-fund-slug'
|
||||
import { signOut, useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover'
|
||||
import { Select, SelectTrigger, SelectValue } from '../../../components/ui/select'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '../../../components/ui/command'
|
||||
import { cn } from '../../../utils/cn'
|
||||
|
||||
const changeProfileFormSchema = z.object({ company: z.string() })
|
||||
|
||||
const changeEmailFormSchema = z.object({ newEmail: z.string().email() })
|
||||
|
||||
const changePasswordFormSchema = z
|
||||
.object({
|
||||
@@ -31,17 +48,54 @@ const changePasswordFormSchema = z
|
||||
path: ['confirmNewPassword'],
|
||||
})
|
||||
|
||||
const changeEmailFormSchema = z.object({ newEmail: z.string().email() })
|
||||
const changeMailingAddressFormSchema = z
|
||||
.object({
|
||||
addressLine1: z.string().min(1),
|
||||
addressLine2: z.string(),
|
||||
city: z.string().min(1),
|
||||
state: z.string(),
|
||||
country: z.string().min(1),
|
||||
zip: z.string().min(1),
|
||||
_addressStateOptionsLength: z.number(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.state && data._addressStateOptionsLength) {
|
||||
ctx.addIssue({
|
||||
path: ['state'],
|
||||
code: 'custom',
|
||||
message: 'State is required.',
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
type ChangePasswordFormInputs = z.infer<typeof changePasswordFormSchema>
|
||||
type ChangeProfileFormInputs = z.infer<typeof changeProfileFormSchema>
|
||||
type ChangeEmailFormInputs = z.infer<typeof changeEmailFormSchema>
|
||||
type ChangePasswordFormInputs = z.infer<typeof changePasswordFormSchema>
|
||||
type ChangeMailingAddressFormInputs = z.infer<typeof changeMailingAddressFormSchema>
|
||||
|
||||
function Settings() {
|
||||
const router = useRouter()
|
||||
const fundSlug = useFundSlug()
|
||||
const session = useSession()
|
||||
const changePasswordMutation = trpc.account.changePassword.useMutation()
|
||||
const getUserAttributesQuery = trpc.account.getUserAttributes.useQuery()
|
||||
const getCountriesQuery = trpc.perk.getCountries.useQuery()
|
||||
const changeProfileMutation = trpc.account.changeProfile.useMutation()
|
||||
const requestEmailChangeMutation = trpc.account.requestEmailChange.useMutation()
|
||||
const changePasswordMutation = trpc.account.changePassword.useMutation()
|
||||
const changeMailingAddressMutation = trpc.account.changeMailingAddress.useMutation()
|
||||
|
||||
const changeProfileForm = useForm<ChangeProfileFormInputs>({
|
||||
resolver: zodResolver(changeProfileFormSchema),
|
||||
defaultValues: { company: '' },
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
const changeEmailForm = useForm<ChangeEmailFormInputs>({
|
||||
resolver: zodResolver(changeEmailFormSchema),
|
||||
defaultValues: { newEmail: '' },
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
const changePasswordForm = useForm<ChangePasswordFormInputs>({
|
||||
resolver: zodResolver(changePasswordFormSchema),
|
||||
@@ -53,35 +107,72 @@ function Settings() {
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
const changeEmailForm = useForm<ChangeEmailFormInputs>({
|
||||
resolver: zodResolver(changeEmailFormSchema),
|
||||
defaultValues: { newEmail: '' },
|
||||
const changeMailingAddressForm = useForm<ChangeMailingAddressFormInputs>({
|
||||
resolver: zodResolver(changeMailingAddressFormSchema),
|
||||
defaultValues: {
|
||||
addressLine1: '',
|
||||
addressLine2: '',
|
||||
zip: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
},
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
async function onChangePasswordSubmit(data: ChangePasswordFormInputs) {
|
||||
const addressCountryOptions = (getCountriesQuery.data || []).map((country) => ({
|
||||
label: country.name,
|
||||
value: country.code,
|
||||
}))
|
||||
|
||||
const addressCountry = changeMailingAddressForm.watch('country')
|
||||
const addressState = changeMailingAddressForm.watch('state')
|
||||
|
||||
const addressStateOptions = useMemo(() => {
|
||||
const selectedCountry = (getCountriesQuery.data || []).find(
|
||||
(country) => country.code === addressCountry
|
||||
)
|
||||
|
||||
const stateOptions =
|
||||
selectedCountry?.states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.code,
|
||||
})) || []
|
||||
|
||||
return stateOptions
|
||||
}, [addressCountry])
|
||||
|
||||
useEffect(() => {
|
||||
changeMailingAddressForm.setValue('state', '')
|
||||
}, [addressCountry])
|
||||
|
||||
useEffect(() => {
|
||||
changeMailingAddressForm.setValue('_addressStateOptionsLength', addressStateOptions.length)
|
||||
}, [addressStateOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!getUserAttributesQuery.data) return
|
||||
|
||||
changeProfileForm.setValue('company', getUserAttributesQuery.data.company)
|
||||
changeMailingAddressForm.setValue('addressLine1', getUserAttributesQuery.data.addressLine1)
|
||||
changeMailingAddressForm.setValue('addressLine2', getUserAttributesQuery.data.addressLine2)
|
||||
changeMailingAddressForm.setValue('country', getUserAttributesQuery.data.addressCountry)
|
||||
changeMailingAddressForm.setValue('city', getUserAttributesQuery.data.addressCity)
|
||||
changeMailingAddressForm.setValue('zip', getUserAttributesQuery.data.addressZip)
|
||||
setTimeout(
|
||||
() => changeMailingAddressForm.setValue('state', getUserAttributesQuery.data.addressState),
|
||||
20
|
||||
)
|
||||
}, [getUserAttributesQuery.data])
|
||||
|
||||
const [countrySelectOpen, setCountrySelectOpen] = useState(false)
|
||||
const [stateSelectOpen, setStateSelectOpen] = useState(false)
|
||||
|
||||
async function onChangeProfileSubmit(data: ChangeProfileFormInputs) {
|
||||
try {
|
||||
await changePasswordMutation.mutateAsync({
|
||||
currentPassword: data.currentPassword,
|
||||
newPassword: data.newPassword,
|
||||
})
|
||||
|
||||
changePasswordForm.reset()
|
||||
|
||||
toast({ title: 'Password successfully changed! Please log in again.' })
|
||||
await signOut({ redirect: false })
|
||||
router.push(`/${fundSlug}/?loginEmail=${session.data?.user.email}`)
|
||||
await changeProfileMutation.mutateAsync(data)
|
||||
toast({ title: 'Your profile has successfully been changed!' })
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
if (errorMessage === 'INVALID_PASSWORD') {
|
||||
return changePasswordForm.setError(
|
||||
'currentPassword',
|
||||
{ message: 'Invalid password.' },
|
||||
{ shouldFocus: true }
|
||||
)
|
||||
}
|
||||
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
@@ -114,6 +205,48 @@ function Settings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangePasswordSubmit(data: ChangePasswordFormInputs) {
|
||||
try {
|
||||
await changePasswordMutation.mutateAsync({
|
||||
currentPassword: data.currentPassword,
|
||||
newPassword: data.newPassword,
|
||||
})
|
||||
|
||||
changePasswordForm.reset()
|
||||
|
||||
toast({ title: 'Your password has successfully been changed! Please log in again.' })
|
||||
await signOut({ redirect: false })
|
||||
router.push(`/${fundSlug}/?loginEmail=${session.data?.user.email}`)
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
if (errorMessage === 'INVALID_PASSWORD') {
|
||||
return changePasswordForm.setError(
|
||||
'currentPassword',
|
||||
{ message: 'Invalid password.' },
|
||||
{ shouldFocus: true }
|
||||
)
|
||||
}
|
||||
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangeMailingAddressSubmit(data: ChangeMailingAddressFormInputs) {
|
||||
try {
|
||||
await changeMailingAddressMutation.mutateAsync(data)
|
||||
toast({ title: 'Your mailing address has successfully been changed!' })
|
||||
} catch (error) {
|
||||
return toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -121,6 +254,72 @@ function Settings() {
|
||||
</Head>
|
||||
|
||||
<div className="w-full max-w-lg mx-auto flex flex-col space-y-12">
|
||||
<div className="w-full flex flex-col space-y-6">
|
||||
<h1 className="font-bold">Change Profile</h1>
|
||||
|
||||
<Form {...changeEmailForm}>
|
||||
<form
|
||||
onSubmit={changeProfileForm.handleSubmit(onChangeProfileSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={changeProfileForm.control}
|
||||
name="company"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Company</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
changeProfileForm.formState.isSubmitting || !changeProfileForm.formState.isValid
|
||||
}
|
||||
>
|
||||
{changeProfileForm.formState.isSubmitting && <Spinner />} Change Profile
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col space-y-6">
|
||||
<h1 className="font-bold">Change Email</h1>
|
||||
|
||||
<Form {...changeEmailForm}>
|
||||
<form
|
||||
onSubmit={changeEmailForm.handleSubmit(onChangeEmailSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={changeEmailForm.control}
|
||||
name="newEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={session.data?.user.email} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
changeEmailForm.formState.isSubmitting || !changeEmailForm.formState.isValid
|
||||
}
|
||||
>
|
||||
{changeEmailForm.formState.isSubmitting && <Spinner />} Change Email
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col space-y-6">
|
||||
<h1 className="font-bold">Change Password</h1>
|
||||
|
||||
@@ -134,7 +333,7 @@ function Settings() {
|
||||
name="currentPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Current password</FormLabel>
|
||||
<FormLabel>Current password *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
@@ -148,7 +347,7 @@ function Settings() {
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>New password</FormLabel>
|
||||
<FormLabel>New password *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
@@ -162,7 +361,7 @@ function Settings() {
|
||||
name="confirmNewPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm new password</FormLabel>
|
||||
<FormLabel>Confirm new password *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
@@ -183,21 +382,197 @@ function Settings() {
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col space-y-6">
|
||||
<h1 className="font-bold">Change Email</h1>
|
||||
<h1 className="font-bold">Change Mailing Address</h1>
|
||||
|
||||
<Form {...changeEmailForm}>
|
||||
<form
|
||||
onSubmit={changeEmailForm.handleSubmit(onChangeEmailSubmit)}
|
||||
onSubmit={changeMailingAddressForm.handleSubmit(onChangeMailingAddressSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={changeEmailForm.control}
|
||||
name="newEmail"
|
||||
control={changeMailingAddressForm.control}
|
||||
name="addressLine1"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>Address line 1 *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={session.data?.user.email} {...field} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={changeMailingAddressForm.control}
|
||||
name="addressLine2"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address line 2</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={changeMailingAddressForm.control}
|
||||
name="country"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Country *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={countrySelectOpen}
|
||||
onOpenChange={(open) => setCountrySelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select
|
||||
open={countrySelectOpen}
|
||||
onValueChange={() => setCountrySelectOpen(false)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
(getCountriesQuery.data || []).find(
|
||||
(country) => country.code === addressCountry
|
||||
)?.name || ''
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search country..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No country found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{addressCountryOptions.map((country) => (
|
||||
<CommandItem
|
||||
value={country.label}
|
||||
key={country.value}
|
||||
onSelect={() => (
|
||||
changeMailingAddressForm.setValue('country', country.value, {
|
||||
shouldValidate: true,
|
||||
}),
|
||||
setCountrySelectOpen(false)
|
||||
)}
|
||||
>
|
||||
{country.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
country.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!!addressStateOptions.length && (
|
||||
<FormField
|
||||
control={changeMailingAddressForm.control}
|
||||
name="state"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>State *</FormLabel>
|
||||
<Popover
|
||||
modal
|
||||
open={stateSelectOpen}
|
||||
onOpenChange={(open) => setStateSelectOpen(open)}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
addressStateOptions.find(
|
||||
(state) => state.value === addressState
|
||||
)?.label
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search state..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No state found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{addressStateOptions.map((state) => (
|
||||
<CommandItem
|
||||
value={state.label}
|
||||
key={state.value}
|
||||
onSelect={() => {
|
||||
console.log('asdasd')
|
||||
changeMailingAddressForm.setValue('state', state.value, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
setStateSelectOpen(false)
|
||||
}}
|
||||
>
|
||||
{state.label}
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
state.value === field.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={changeMailingAddressForm.control}
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>City *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={changeMailingAddressForm.control}
|
||||
name="zip"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postal code *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -206,10 +581,12 @@ function Settings() {
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
changeEmailForm.formState.isSubmitting || !changeEmailForm.formState.isValid
|
||||
changeMailingAddressForm.formState.isSubmitting ||
|
||||
!changeMailingAddressForm.formState.isValid
|
||||
}
|
||||
>
|
||||
{changeEmailForm.formState.isSubmitting && <Spinner />} Change Email
|
||||
{changeMailingAddressForm.formState.isSubmitting && <Spinner />} Change Mailing
|
||||
Address
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -72,10 +72,18 @@
|
||||
"description": "${role_default-roles}",
|
||||
"composite": true,
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"realm": [
|
||||
"offline_access",
|
||||
"uma_authorization"
|
||||
],
|
||||
"client": {
|
||||
"realm-management": ["manage-users"],
|
||||
"account": ["view-profile", "manage-account"]
|
||||
"realm-management": [
|
||||
"manage-users"
|
||||
],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account"
|
||||
]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@@ -129,7 +137,10 @@
|
||||
"composite": true,
|
||||
"composites": {
|
||||
"client": {
|
||||
"realm-management": ["query-users", "query-groups"]
|
||||
"realm-management": [
|
||||
"query-users",
|
||||
"query-groups"
|
||||
]
|
||||
}
|
||||
},
|
||||
"clientRole": true,
|
||||
@@ -275,7 +286,9 @@
|
||||
"composite": true,
|
||||
"composites": {
|
||||
"client": {
|
||||
"realm-management": ["query-clients"]
|
||||
"realm-management": [
|
||||
"query-clients"
|
||||
]
|
||||
}
|
||||
},
|
||||
"clientRole": true,
|
||||
@@ -323,7 +336,9 @@
|
||||
"composite": true,
|
||||
"composites": {
|
||||
"client": {
|
||||
"account": ["view-consent"]
|
||||
"account": [
|
||||
"view-consent"
|
||||
]
|
||||
}
|
||||
},
|
||||
"clientRole": true,
|
||||
@@ -382,7 +397,9 @@
|
||||
"composite": true,
|
||||
"composites": {
|
||||
"client": {
|
||||
"account": ["manage-account-links"]
|
||||
"account": [
|
||||
"manage-account-links"
|
||||
]
|
||||
}
|
||||
},
|
||||
"clientRole": true,
|
||||
@@ -401,7 +418,9 @@
|
||||
"clientRole": false,
|
||||
"containerId": "69206f5b-3557-4d79-aa26-e42faeaa6004"
|
||||
},
|
||||
"requiredCredentials": ["password"],
|
||||
"requiredCredentials": [
|
||||
"password"
|
||||
],
|
||||
"otpPolicyType": "totp",
|
||||
"otpPolicyAlgorithm": "HmacSHA1",
|
||||
"otpPolicyInitialCounter": 0,
|
||||
@@ -416,7 +435,9 @@
|
||||
],
|
||||
"localizationTexts": {},
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicySignatureAlgorithms": [
|
||||
"ES256"
|
||||
],
|
||||
"webAuthnPolicyRpId": "",
|
||||
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
||||
@@ -427,7 +448,9 @@
|
||||
"webAuthnPolicyAcceptableAaguids": [],
|
||||
"webAuthnPolicyExtraOrigins": [],
|
||||
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
|
||||
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyPasswordlessSignatureAlgorithms": [
|
||||
"ES256"
|
||||
],
|
||||
"webAuthnPolicyPasswordlessRpId": "",
|
||||
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
|
||||
@@ -448,7 +471,9 @@
|
||||
"serviceAccountClientId": "app",
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-magicgrants"],
|
||||
"realmRoles": [
|
||||
"default-roles-magicgrants"
|
||||
],
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@@ -456,14 +481,19 @@
|
||||
"scopeMappings": [
|
||||
{
|
||||
"clientScope": "offline_access",
|
||||
"roles": ["offline_access"]
|
||||
"roles": [
|
||||
"offline_access"
|
||||
]
|
||||
}
|
||||
],
|
||||
"clientScopeMappings": {
|
||||
"account": [
|
||||
{
|
||||
"client": "account-console",
|
||||
"roles": ["manage-account", "view-groups"]
|
||||
"roles": [
|
||||
"manage-account",
|
||||
"view-groups"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -478,7 +508,9 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": ["/realms/magic/account/*"],
|
||||
"redirectUris": [
|
||||
"/realms/magic/account/*"
|
||||
],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
@@ -496,7 +528,9 @@
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"defaultClientScopes": ["basic"],
|
||||
"defaultClientScopes": [
|
||||
"basic"
|
||||
],
|
||||
"optionalClientScopes": []
|
||||
},
|
||||
{
|
||||
@@ -509,7 +543,9 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": ["/realms/magic/account/*"],
|
||||
"redirectUris": [
|
||||
"/realms/magic/account/*"
|
||||
],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
@@ -538,7 +574,9 @@
|
||||
"config": {}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": ["basic"],
|
||||
"defaultClientScopes": [
|
||||
"basic"
|
||||
],
|
||||
"optionalClientScopes": []
|
||||
},
|
||||
{
|
||||
@@ -567,7 +605,9 @@
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"defaultClientScopes": ["basic"],
|
||||
"defaultClientScopes": [
|
||||
"basic"
|
||||
],
|
||||
"optionalClientScopes": []
|
||||
},
|
||||
{
|
||||
@@ -583,8 +623,12 @@
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "**********",
|
||||
"redirectUris": ["/*"],
|
||||
"webOrigins": ["/*"],
|
||||
"redirectUris": [
|
||||
"/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"/*"
|
||||
],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@@ -597,7 +641,7 @@
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"client.secret.creation.time": "1724090232",
|
||||
"client.secret.creation.time": "1729548068",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
@@ -656,8 +700,20 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": ["web-origins", "acr", "roles", "profile", "basic", "email"],
|
||||
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
|
||||
"defaultClientScopes": [
|
||||
"web-origins",
|
||||
"acr",
|
||||
"roles",
|
||||
"profile",
|
||||
"basic",
|
||||
"email"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"offline_access",
|
||||
"microprofile-jwt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b44227a1-ce19-41b0-8b61-21ce79039e35",
|
||||
@@ -727,8 +783,12 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": ["/admin/magic/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"redirectUris": [
|
||||
"/admin/magic/console/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"+"
|
||||
],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@@ -764,7 +824,9 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": ["basic"],
|
||||
"defaultClientScopes": [
|
||||
"basic"
|
||||
],
|
||||
"optionalClientScopes": []
|
||||
}
|
||||
],
|
||||
@@ -1356,7 +1418,12 @@
|
||||
"acr",
|
||||
"basic"
|
||||
],
|
||||
"defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"],
|
||||
"defaultOptionalClientScopes": [
|
||||
"offline_access",
|
||||
"address",
|
||||
"phone",
|
||||
"microprofile-jwt"
|
||||
],
|
||||
"browserSecurityHeaders": {
|
||||
"contentSecurityPolicyReportOnly": "",
|
||||
"xContentTypeOptions": "nosniff",
|
||||
@@ -1369,7 +1436,9 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"eventsListeners": [
|
||||
"jboss-logging"
|
||||
],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@@ -1384,7 +1453,9 @@
|
||||
"subType": "anonymous",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"max-clients": ["200"]
|
||||
"max-clients": [
|
||||
"200"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1404,13 +1475,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -1429,7 +1500,9 @@
|
||||
"subType": "anonymous",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allow-default-scopes": ["true"]
|
||||
"allow-default-scopes": [
|
||||
"true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1439,8 +1512,12 @@
|
||||
"subType": "anonymous",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"host-sending-registration-request-must-match": ["true"],
|
||||
"client-uris-must-match": ["true"]
|
||||
"host-sending-registration-request-must-match": [
|
||||
"true"
|
||||
],
|
||||
"client-uris-must-match": [
|
||||
"true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1451,13 +1528,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper"
|
||||
]
|
||||
}
|
||||
@@ -1469,7 +1546,9 @@
|
||||
"subType": "authenticated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allow-default-scopes": ["true"]
|
||||
"allow-default-scopes": [
|
||||
"true"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1480,7 +1559,7 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kc.user.profile.config": [
|
||||
"{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"name\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"emailVerifyTokenVersion\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"passwordResetTokenVersion\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeMoneroCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeFiroCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripePgCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeGeneralCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
|
||||
"{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"name\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"addressLine1\",\"displayName\":\"Address line 1\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"addressLine2\",\"displayName\":\"Address line 2\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"addressZip\",\"displayName\":\"Address zip\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"addressCity\",\"displayName\":\"Address city\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"addressState\",\"displayName\":\"Address state\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"addressCountry\",\"displayName\":\"Address country\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"group\":\"mailingAddress\",\"multivalued\":false},{\"name\":\"emailVerifyTokenVersion\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"passwordResetTokenVersion\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeMoneroCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeFiroCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripePgCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"stripeGeneralCustomerId\",\"displayName\":\"\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"company\",\"displayName\":\"Company\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"},{\"name\":\"mailingAddress\",\"displayHeader\":\"Mailing address\",\"displayDescription\":\"\",\"annotations\":{}}]}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1492,8 +1571,12 @@
|
||||
"providerId": "rsa-enc-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"priority": ["100"],
|
||||
"algorithm": ["RSA-OAEP"]
|
||||
"priority": [
|
||||
"100"
|
||||
],
|
||||
"algorithm": [
|
||||
"RSA-OAEP"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1502,7 +1585,9 @@
|
||||
"providerId": "aes-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"priority": ["100"]
|
||||
"priority": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1511,7 +1596,9 @@
|
||||
"providerId": "rsa-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"priority": ["100"]
|
||||
"priority": [
|
||||
"100"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1520,8 +1607,12 @@
|
||||
"providerId": "hmac-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"priority": ["100"],
|
||||
"algorithm": ["HS512"]
|
||||
"priority": [
|
||||
"100"
|
||||
],
|
||||
"algorithm": [
|
||||
"HS512"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2199,4 +2290,4 @@
|
||||
"clientPolicies": {
|
||||
"policies": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,36 @@ import { authenticateKeycloakClient } from '../utils/keycloak'
|
||||
import { fundSlugs } from '../../utils/funds'
|
||||
|
||||
export const accountRouter = router({
|
||||
changeProfile: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
company: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
const userId = ctx.session.user.sub
|
||||
const user = await keycloak.users.findOne({ id: userId })
|
||||
|
||||
if (!user || !user.id)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'USER_NOT_FOUND',
|
||||
})
|
||||
|
||||
await keycloak.users.update(
|
||||
{ id: userId },
|
||||
{
|
||||
...user,
|
||||
attributes: {
|
||||
...user.attributes,
|
||||
company: input.company,
|
||||
},
|
||||
}
|
||||
)
|
||||
}),
|
||||
|
||||
changePassword: protectedProcedure
|
||||
.input(z.object({ currentPassword: z.string().min(1), newPassword: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -119,4 +149,67 @@ export const accountRouter = router({
|
||||
html: `<a href="${env.APP_URL}/${input.fundSlug}/verify-email/${token}" target="_blank">Verify email</a>`,
|
||||
})
|
||||
}),
|
||||
|
||||
changeMailingAddress: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
addressLine1: z.string().min(1),
|
||||
addressLine2: z.string(),
|
||||
city: z.string().min(1),
|
||||
state: z.string(),
|
||||
country: z.string().min(1),
|
||||
zip: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
const userId = ctx.session.user.sub
|
||||
const user = await keycloak.users.findOne({ id: userId })
|
||||
|
||||
if (!user || !user.id)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'USER_NOT_FOUND',
|
||||
})
|
||||
|
||||
await keycloak.users.update(
|
||||
{ id: userId },
|
||||
{
|
||||
...user,
|
||||
attributes: {
|
||||
...user.attributes,
|
||||
addressLine1: input.addressLine1,
|
||||
addressLine2: input.addressLine2,
|
||||
addressZip: input.zip,
|
||||
addressCity: input.city,
|
||||
addressState: input.state,
|
||||
addressCountry: input.country,
|
||||
},
|
||||
}
|
||||
)
|
||||
}),
|
||||
|
||||
getUserAttributes: protectedProcedure.query(async ({ ctx }) => {
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
const userId = ctx.session.user.sub
|
||||
const user = await keycloak.users.findOne({ id: userId })
|
||||
|
||||
if (!user || !user.id)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'USER_NOT_FOUND',
|
||||
})
|
||||
|
||||
return {
|
||||
company: (user.attributes?.company?.[0] as string) || '',
|
||||
addressLine1: (user.attributes?.addressLine1?.[0] as string) || '',
|
||||
addressLine2: (user.attributes?.addressLine2?.[0] as string) || '',
|
||||
addressZip: (user.attributes?.addressZip?.[0] as string) || '',
|
||||
addressCity: (user.attributes?.addressCity?.[0] as string) || '',
|
||||
addressState: (user.attributes?.addressState?.[0] as string) || '',
|
||||
addressCountry: (user.attributes?.addressCountry?.[0] as string) || '',
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -12,21 +12,83 @@ import { UserSettingsJwtPayload } from '../types'
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-záéíóúÁÉÍÓÚñÑçÇ]+$/, 'Use alphabetic characters only.'),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-záéíóúÁÉÍÓÚñÑçÇ]+$/, 'Use alphabetic characters only.'),
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
fundSlug: z.enum(fundSlugs),
|
||||
})
|
||||
z
|
||||
.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-záéíóúÁÉÍÓÚñÑçÇ]+$/, 'Use alphabetic characters only.'),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[A-Za-záéíóúÁÉÍÓÚñÑçÇ]+$/, 'Use alphabetic characters only.'),
|
||||
company: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
fundSlug: z.enum(fundSlugs),
|
||||
_addMailingAddress: z.boolean(),
|
||||
address: z
|
||||
.object({
|
||||
addressLine1: z.string(),
|
||||
addressLine2: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string(),
|
||||
country: z.string(),
|
||||
zip: z.string(),
|
||||
_addressStateOptionsLength: z.number(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.state && data._addressStateOptionsLength) {
|
||||
ctx.addIssue({
|
||||
path: ['shippingState'],
|
||||
code: 'custom',
|
||||
message: 'State is required.',
|
||||
})
|
||||
}
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match.',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data._addMailingAddress) {
|
||||
if (!data.address.addressLine1) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.addressLine1'],
|
||||
code: 'custom',
|
||||
message: 'Address line 1 is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.country) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.country'],
|
||||
code: 'custom',
|
||||
message: 'Country is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.city) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.city'],
|
||||
code: 'custom',
|
||||
message: 'City is required.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.address.zip) {
|
||||
ctx.addIssue({
|
||||
path: ['shipping.zip'],
|
||||
code: 'custom',
|
||||
message: 'Postal code is required.',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await authenticateKeycloakClient()
|
||||
@@ -43,6 +105,13 @@ export const authRouter = router({
|
||||
name: `${input.firstName} ${input.lastName}`,
|
||||
passwordResetTokenVersion: 1,
|
||||
emailVerifyTokenVersion: 1,
|
||||
company: input.company,
|
||||
addressLine1: input._addMailingAddress ? input.address.addressLine1 : '',
|
||||
addressLine2: input._addMailingAddress ? input.address.addressLine2 : '',
|
||||
addressCountry: input._addMailingAddress ? input.address.country : '',
|
||||
addressState: input._addMailingAddress ? input.address.state : '',
|
||||
addressCity: input._addMailingAddress ? input.address.city : '',
|
||||
addressZip: input._addMailingAddress ? input.address.zip : '',
|
||||
},
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
@@ -4,23 +4,14 @@ import { QueueEvents } from 'bullmq'
|
||||
import { fundSlugs } from '../../utils/funds'
|
||||
import { keycloak, printfulApi, prisma, strapiApi } from '../services'
|
||||
import {
|
||||
PrintfulCreateOrderReq,
|
||||
PrintfulCreateOrderRes,
|
||||
PrintfulEstimateOrderReq,
|
||||
PrintfulEstimateOrderRes,
|
||||
PrintfulGetCountriesRes,
|
||||
PrintfulGetProductRes,
|
||||
StrapiCreateOrderBody,
|
||||
StrapiCreateOrderRes,
|
||||
StrapiCreatePointBody,
|
||||
StrapiGetPerkRes,
|
||||
StrapiGetPerksPopulatedRes,
|
||||
StrapiGetPointsPopulatedRes,
|
||||
StrapiPerk,
|
||||
} from '../types'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { estimatePrintfulOrderCost, getUserPointBalance } from '../utils/perks'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { POINTS_REDEEM_PRICE_USD } from '../../config'
|
||||
import { authenticateKeycloakClient } from '../utils/keycloak'
|
||||
import { perkPurchaseQueue } from '../queues'
|
||||
@@ -68,7 +59,7 @@ export const perkRouter = router({
|
||||
return printfulProduct.sync_variants
|
||||
}),
|
||||
|
||||
getCountries: protectedProcedure.query(async () => {
|
||||
getCountries: publicProcedure.query(async () => {
|
||||
const {
|
||||
data: { result: countries },
|
||||
} = await printfulApi.get<PrintfulGetCountriesRes>('/countries')
|
||||
|
||||
Reference in New Issue
Block a user