1- import { useCallback , useEffect , useState } from 'react'
1+ import { useCallback , useEffect , useMemo , useState } from 'react'
22
33import {
44 DEVELOPER_APP_DESCRIPTION_MAX_LENGTH ,
@@ -17,9 +17,11 @@ import {
1717 Button ,
1818 Flex ,
1919 IconEmbed ,
20- Divider
20+ Divider ,
21+ IconPlus ,
22+ Text
2123} from '@audius/harmony'
22- import { Form , Formik , useField } from 'formik'
24+ import { FieldArray , Form , Formik , useField } from 'formik'
2325import { z } from 'zod'
2426import { toFormikValidationSchema } from 'zod-formik-adapter'
2527
@@ -28,6 +30,7 @@ import { TextAreaField, TextField } from 'components/form-fields'
2830import PreloadImage from 'components/preload-image/PreloadImage'
2931import Toast from 'components/toast/Toast'
3032import { copyToClipboard } from 'utils/clipboardUtil'
33+ import { removeNullable } from 'utils/typeUtils'
3134
3235import styles from './EditAppPage.module.css'
3336import { MaskedSecretDisplay } from './MaskedSecretDisplay'
@@ -53,7 +56,13 @@ const messages = {
5356 back : 'Back' ,
5457 save : 'Save Changes' ,
5558 saving : 'Saving' ,
56- miscError : 'Sorry, something went wrong. Please try again later.'
59+ miscError : 'Sorry, something went wrong. Please try again later.' ,
60+ redirectUrisLabel : 'Registered Callback URLs' ,
61+ redirectUrisHelp :
62+ 'Allowed values for the redirect_uri query parameter when using OAuth2 to obtain user access tokens.' ,
63+ removeRedirectUri : 'Remove redirect URI' ,
64+ addRedirectUri : 'Add Redirect URI' ,
65+ redirectUriPlaceholder : 'https://example.com/callback'
5766}
5867
5968const ImageField = ( { name } : { name : string } ) => {
@@ -89,7 +98,7 @@ const getBearerTokens = (params: EditAppPageProps['params']) => {
8998
9099export const EditAppPage = ( props : EditAppPageProps ) => {
91100 const { params, setPage } = props
92- const { name, description , apiKey, imageUrl } = params ?? { }
101+ const { name, apiKey } = params ?? { }
93102 const initialBearerTokens = getBearerTokens ( params )
94103 const [ bearerTokens , setBearerTokens ] =
95104 useState < string [ ] > ( initialBearerTokens )
@@ -99,7 +108,6 @@ export const EditAppPage = (props: EditAppPageProps) => {
99108 const { isSuccess, isError, error, mutate, isPending } = useEditDeveloperApp ( )
100109 const deactivateAccessKey = useDeactivateDeveloperAppAccessKey ( )
101110 const createAccessKey = useCreateDeveloperAppAccessKey ( )
102- const [ submitError , setSubmitError ] = useState < string | null > ( null )
103111
104112 // Sync bearer tokens when params change (e.g. navigating to different app)
105113 useEffect ( ( ) => {
@@ -120,7 +128,6 @@ export const EditAppPage = (props: EditAppPageProps) => {
120128
121129 useEffect ( ( ) => {
122130 if ( isError ) {
123- setSubmitError ( messages . miscError )
124131 record (
125132 make ( Name . DEVELOPER_APP_EDIT_ERROR , {
126133 error : error ?. message
@@ -131,24 +138,33 @@ export const EditAppPage = (props: EditAppPageProps) => {
131138
132139 const handleSubmit = useCallback (
133140 ( values : DeveloperAppValues ) => {
134- setSubmitError ( null )
135141 record (
136142 make ( Name . DEVELOPER_APP_EDIT_SUBMIT , {
137143 name : values . name ,
138144 description : values . description
139145 } )
140146 )
141- mutate ( values )
147+ // Trim redirect URIs and remove empty ones
148+ const redirectUris = ( values . redirectUris ?? [ ] )
149+ . map ( ( u ) => u ?. trim ( ) )
150+ . filter ( removeNullable )
151+ // Trim image URL and set to undefined if empty string
152+ const imageUrl = values . imageUrl ?. trim ( ) || undefined
153+ mutate ( { ...values , redirectUris, imageUrl } )
142154 } ,
143155 [ mutate , record ]
144156 )
145157
146- const initialValues : DeveloperAppValues = {
147- apiKey : apiKey || '' ,
148- name : name || '' ,
149- description,
150- imageUrl
151- }
158+ const initialValues : DeveloperAppValues = useMemo (
159+ ( ) => ( {
160+ apiKey : params ?. apiKey || '' ,
161+ name : params ?. name || '' ,
162+ description : params ?. description ,
163+ imageUrl : params ?. imageUrl ,
164+ redirectUris : params ?. redirectUris ?. length ? params . redirectUris : [ '' ]
165+ } ) ,
166+ [ params ]
167+ )
152168
153169 const copyApiKey = useCallback ( ( ) => {
154170 if ( ! apiKey ) return
@@ -188,6 +204,7 @@ export const EditAppPage = (props: EditAppPageProps) => {
188204 initialValues = { initialValues }
189205 onSubmit = { handleSubmit }
190206 validationSchema = { toFormikValidationSchema ( developerAppEditSchema ) }
207+ enableReinitialize
191208 >
192209 < Form >
193210 < Flex gap = 'm' direction = 'column' >
@@ -281,6 +298,51 @@ export const EditAppPage = (props: EditAppPageProps) => {
281298 >
282299 { messages . createNewToken }
283300 </ Button >
301+ < Flex direction = 'column' gap = 's' >
302+ < Text variant = 'body' strength = 'strong' >
303+ { messages . redirectUrisLabel }
304+ </ Text >
305+ < Text variant = 'body' size = 's' color = 'subdued' >
306+ { messages . redirectUrisHelp }
307+ </ Text >
308+ < FieldArray name = 'redirectUris' >
309+ { ( { push, remove, form } ) => {
310+ const uris : string [ ] = form . values . redirectUris
311+ return (
312+ < >
313+ { uris . map ( ( uri , index ) => {
314+ const isLast = index === uris . length - 1
315+ return (
316+ < Flex key = { index } gap = 's' alignItems = 'center' >
317+ < TextField
318+ name = { `redirectUris.${ index } ` }
319+ label = { `${ messages . addRedirectUri } ${ index + 1 } ` }
320+ placeholder = { messages . redirectUriPlaceholder }
321+ disabled = { isPending }
322+ />
323+ { isLast ? (
324+ < IconButton
325+ onClick = { ( ) => push ( '' ) }
326+ aria-label = { messages . addRedirectUri }
327+ color = 'default'
328+ icon = { IconPlus }
329+ />
330+ ) : (
331+ < IconButton
332+ onClick = { ( ) => remove ( index ) }
333+ aria-label = { messages . removeRedirectUri }
334+ color = 'subdued'
335+ icon = { IconTrash }
336+ />
337+ ) }
338+ </ Flex >
339+ )
340+ } ) }
341+ </ >
342+ )
343+ } }
344+ </ FieldArray >
345+ </ Flex >
284346 < div className = { styles . actionsContainer } >
285347 < Button
286348 variant = 'secondary'
@@ -300,11 +362,11 @@ export const EditAppPage = (props: EditAppPageProps) => {
300362 { isPending ? messages . saving : messages . save }
301363 </ Button >
302364 </ div >
303- { submitError == null ? null : (
365+ { isError ? (
304366 < div className = { styles . errorContainer } >
305367 < span className = { styles . errorText } > { messages . miscError } </ span >
306368 </ div >
307- ) }
369+ ) : null }
308370 </ Flex >
309371 </ Form >
310372 </ Formik >
0 commit comments