@@ -4,13 +4,13 @@ import type { ExternalAccountResource, OAuthProvider, OAuthScope, OAuthStrategy
4
4
import { appendModalState } from '../../../utils' ;
5
5
import { ProviderInitialIcon } from '../../common' ;
6
6
import { useUserProfileContext } from '../../contexts' ;
7
- import { Badge , Box , descriptors , Flex , Image , localizationKeys , Text } from '../../customizables' ;
7
+ import { Box , Button , descriptors , Flex , Image , localizationKeys , Text } from '../../customizables' ;
8
8
import { Card , ProfileSection , ThreeDotsMenu , useCardState , withCardStateProvider } from '../../elements' ;
9
9
import { Action } from '../../elements/Action' ;
10
10
import { useActionContext } from '../../elements/Action/ActionRoot' ;
11
11
import { useEnabledThirdPartyProviders } from '../../hooks' ;
12
12
import { useRouter } from '../../router' ;
13
- import { type PropsOfComponent } from '../../styledSystem' ;
13
+ import type { PropsOfComponent } from '../../styledSystem' ;
14
14
import { handleError } from '../../utils' ;
15
15
import { AddConnectedAccount } from './ConnectedAccountsMenu' ;
16
16
import { RemoveConnectedAccountForm } from './RemoveResourceForm' ;
@@ -27,11 +27,28 @@ const RemoveConnectedAccountScreen = (props: RemoveConnectedAccountScreenProps)
27
27
) ;
28
28
} ;
29
29
30
+ const errorCodesForReconnect = [
31
+ /**
32
+ * Some Oauth providers will generate a refresh token only the first time the user gives consent to the app.
33
+ */
34
+ 'external_account_missing_refresh_token' ,
35
+ /**
36
+ * Provider is experiencing an issue currently.
37
+ */
38
+ 'oauth_fetch_user_error' ,
39
+ /**
40
+ * Provider is experiencing an issue currently (same as above).
41
+ */
42
+ 'oauth_token_exchange_error' ,
43
+ /**
44
+ * User's associated email address is required to be verified, because it was initially created as unverified.
45
+ */
46
+ 'external_account_email_address_verification_required' ,
47
+ ] ;
48
+
30
49
export const ConnectedAccountsSection = withCardStateProvider ( ( ) => {
31
50
const { user } = useUser ( ) ;
32
51
const card = useCardState ( ) ;
33
- const { providerToDisplayData } = useEnabledThirdPartyProviders ( ) ;
34
- const { additionalOAuthScopes } = useUserProfileContext ( ) ;
35
52
36
53
if ( ! user ) {
37
54
return null ;
@@ -51,79 +68,12 @@ export const ConnectedAccountsSection = withCardStateProvider(() => {
51
68
< Card . Alert > { card . error } </ Card . Alert >
52
69
< Action . Root >
53
70
< ProfileSection . ItemList id = 'connectedAccounts' >
54
- { accounts . map ( account => {
55
- const label = account . username || account . emailAddress ;
56
- const error = account . verification ?. error ?. longMessage ;
57
- const additionalScopes = findAdditionalScopes ( account , additionalOAuthScopes ) ;
58
- const reauthorizationRequired = additionalScopes . length > 0 && account . approvedScopes != '' ;
59
- const errorMessage = ! reauthorizationRequired
60
- ? error
61
- : localizationKeys ( 'userProfile.start.connectedAccountsSection.subtitle__reauthorize' ) ;
62
-
63
- const ImageOrInitial = ( ) =>
64
- providerToDisplayData [ account . provider ] . iconUrl ? (
65
- < Image
66
- elementDescriptor = { [ descriptors . providerIcon ] }
67
- elementId = { descriptors . socialButtonsProviderIcon . setId ( account . provider ) }
68
- alt = { providerToDisplayData [ account . provider ] . name }
69
- src = { providerToDisplayData [ account . provider ] . iconUrl }
70
- sx = { theme => ( { width : theme . sizes . $4 , flexShrink : 0 } ) }
71
- />
72
- ) : (
73
- < ProviderInitialIcon
74
- id = { account . provider }
75
- value = { providerToDisplayData [ account . provider ] . name }
76
- />
77
- ) ;
78
-
79
- return (
80
- < Action . Root key = { account . id } >
81
- < ProfileSection . Item id = 'connectedAccounts' >
82
- < Flex sx = { t => ( { overflow : 'hidden' , gap : t . space . $2 } ) } >
83
- < ImageOrInitial />
84
- < Box sx = { { whiteSpace : 'nowrap' , overflow : 'hidden' } } >
85
- < Flex
86
- gap = { 2 }
87
- center
88
- >
89
- < Text sx = { t => ( { color : t . colors . $colorText } ) } > { `${
90
- providerToDisplayData [ account . provider ] . name
91
- } `} </ Text >
92
- < Text
93
- truncate
94
- as = 'span'
95
- colorScheme = 'secondary'
96
- >
97
- { label ? `• ${ label } ` : '' }
98
- </ Text >
99
- { ( error || reauthorizationRequired ) && (
100
- < Badge
101
- colorScheme = 'danger'
102
- localizationKey = { localizationKeys ( 'badge__requiresAction' ) }
103
- />
104
- ) }
105
- </ Flex >
106
- </ Box >
107
- </ Flex >
108
-
109
- < ConnectedAccountMenu account = { account } />
110
- </ ProfileSection . Item >
111
- { ( error || reauthorizationRequired ) && (
112
- < Text
113
- colorScheme = 'danger'
114
- sx = { t => ( { padding : `${ t . sizes . $none } ${ t . sizes . $4 } ${ t . sizes . $1x5 } ${ t . sizes . $8x5 } ` } ) }
115
- localizationKey = { errorMessage }
116
- />
117
- ) }
118
-
119
- < Action . Open value = 'remove' >
120
- < Action . Card variant = 'destructive' >
121
- < RemoveConnectedAccountScreen accountId = { account . id } />
122
- </ Action . Card >
123
- </ Action . Open >
124
- </ Action . Root >
125
- ) ;
126
- } ) }
71
+ { accounts . map ( account => (
72
+ < ConnectedAccount
73
+ key = { account . id }
74
+ account = { account }
75
+ />
76
+ ) ) }
127
77
</ ProfileSection . ItemList >
128
78
129
79
< AddConnectedAccount />
@@ -132,32 +82,37 @@ export const ConnectedAccountsSection = withCardStateProvider(() => {
132
82
) ;
133
83
} ) ;
134
84
135
- const ConnectedAccountMenu = ( { account } : { account : ExternalAccountResource } ) => {
136
- const card = useCardState ( ) ;
137
- const { user } = useUser ( ) ;
138
- const { navigate } = useRouter ( ) ;
139
- const { open } = useActionContext ( ) ;
140
- const error = account . verification ?. error ?. longMessage ;
85
+ const ConnectedAccount = ( { account } : { account : ExternalAccountResource } ) => {
141
86
const { additionalOAuthScopes, componentName, mode } = useUserProfileContext ( ) ;
87
+ const { navigate } = useRouter ( ) ;
88
+ const { user } = useUser ( ) ;
89
+ const card = useCardState ( ) ;
90
+
91
+ if ( ! user ) {
92
+ return null ;
93
+ }
94
+
142
95
const isModal = mode === 'modal' ;
96
+ const { providerToDisplayData } = useEnabledThirdPartyProviders ( ) ;
97
+ const label = account . username || account . emailAddress ;
98
+ const fallbackErrorMessage = account . verification ?. error ?. longMessage ;
143
99
const additionalScopes = findAdditionalScopes ( account , additionalOAuthScopes ) ;
144
100
const reauthorizationRequired = additionalScopes . length > 0 && account . approvedScopes != '' ;
145
- const actionLabel = ! reauthorizationRequired
146
- ? localizationKeys ( 'userProfile.start.connectedAccountsSection.actionLabel__connectionFailed' )
147
- : localizationKeys ( 'userProfile.start.connectedAccountsSection.actionLabel__reauthorize' ) ;
101
+ const shouldDisplayReconnect =
102
+ errorCodesForReconnect . includes ( account . verification ?. error ?. code || '' ) || reauthorizationRequired ;
148
103
149
- const handleOnClick = async ( ) => {
104
+ const connectedAccountErrorMessage = shouldDisplayReconnect
105
+ ? localizationKeys ( `userProfile.start.connectedAccountsSection.subtitle__disconnected` )
106
+ : fallbackErrorMessage ;
107
+
108
+ const reconnect = async ( ) => {
150
109
const redirectUrl = isModal ? appendModalState ( { url : window . location . href , componentName } ) : window . location . href ;
151
110
152
111
try {
153
112
let response : ExternalAccountResource ;
154
113
if ( reauthorizationRequired ) {
155
114
response = await account . reauthorize ( { additionalScopes, redirectUrl } ) ;
156
115
} else {
157
- if ( ! user ) {
158
- throw Error ( 'user is not defined' ) ;
159
- }
160
-
161
116
response = await user . createExternalAccount ( {
162
117
strategy : account . verification ! . strategy as OAuthStrategy ,
163
118
redirectUrl,
@@ -171,14 +126,101 @@ const ConnectedAccountMenu = ({ account }: { account: ExternalAccountResource })
171
126
}
172
127
} ;
173
128
129
+ const ImageOrInitial = ( ) =>
130
+ providerToDisplayData [ account . provider ] . iconUrl ? (
131
+ < Image
132
+ elementDescriptor = { [ descriptors . providerIcon ] }
133
+ elementId = { descriptors . socialButtonsProviderIcon . setId ( account . provider ) }
134
+ alt = { providerToDisplayData [ account . provider ] . name }
135
+ src = { providerToDisplayData [ account . provider ] . iconUrl }
136
+ sx = { theme => ( { width : theme . sizes . $4 , flexShrink : 0 } ) }
137
+ />
138
+ ) : (
139
+ < ProviderInitialIcon
140
+ id = { account . provider }
141
+ value = { providerToDisplayData [ account . provider ] . name }
142
+ />
143
+ ) ;
144
+
145
+ return (
146
+ < Action . Root key = { account . id } >
147
+ < ProfileSection . Item id = 'connectedAccounts' >
148
+ < Flex sx = { t => ( { overflow : 'hidden' , gap : t . space . $2 } ) } >
149
+ < ImageOrInitial />
150
+ < Box sx = { { whiteSpace : 'nowrap' , overflow : 'hidden' } } >
151
+ < Flex
152
+ gap = { 2 }
153
+ center
154
+ >
155
+ < Text sx = { t => ( { color : t . colors . $colorText } ) } > { `${
156
+ providerToDisplayData [ account . provider ] . name
157
+ } `} </ Text >
158
+ < Text
159
+ truncate
160
+ as = 'span'
161
+ colorScheme = 'secondary'
162
+ >
163
+ { label ? `• ${ label } ` : '' }
164
+ </ Text >
165
+ </ Flex >
166
+ </ Box >
167
+ </ Flex >
168
+
169
+ < ConnectedAccountMenu />
170
+ </ ProfileSection . Item >
171
+ { shouldDisplayReconnect && (
172
+ < Box
173
+ sx = { t => ( {
174
+ padding : `${ t . sizes . $none } ${ t . sizes . $none } ${ t . sizes . $1x5 } ${ t . sizes . $8x5 } ` ,
175
+ } ) }
176
+ >
177
+ < Text
178
+ colorScheme = 'secondary'
179
+ sx = { t => ( {
180
+ paddingRight : t . sizes . $1x5 ,
181
+ display : 'inline-block' ,
182
+ } ) }
183
+ localizationKey = { connectedAccountErrorMessage }
184
+ />
185
+
186
+ < Button
187
+ sx = { {
188
+ display : 'inline-block' ,
189
+ } }
190
+ onClick = { reconnect }
191
+ variant = 'link'
192
+ localizationKey = { localizationKeys (
193
+ 'userProfile.start.connectedAccountsSection.actionLabel__connectionFailed' ,
194
+ ) }
195
+ />
196
+ </ Box >
197
+ ) }
198
+
199
+ { account . verification ?. error ?. code && ! shouldDisplayReconnect && (
200
+ < Text
201
+ colorScheme = 'danger'
202
+ sx = { t => ( {
203
+ padding : `${ t . sizes . $none } ${ t . sizes . $1x5 } ${ t . sizes . $1x5 } ${ t . sizes . $8x5 } ` ,
204
+ } ) }
205
+ >
206
+ { fallbackErrorMessage }
207
+ </ Text >
208
+ ) }
209
+
210
+ < Action . Open value = 'remove' >
211
+ < Action . Card variant = 'destructive' >
212
+ < RemoveConnectedAccountScreen accountId = { account . id } />
213
+ </ Action . Card >
214
+ </ Action . Open >
215
+ </ Action . Root >
216
+ ) ;
217
+ } ;
218
+
219
+ const ConnectedAccountMenu = ( ) => {
220
+ const { open } = useActionContext ( ) ;
221
+
174
222
const actions = (
175
223
[
176
- error || reauthorizationRequired
177
- ? {
178
- label : actionLabel ,
179
- onClick : handleOnClick ,
180
- }
181
- : null ,
182
224
{
183
225
label : localizationKeys ( 'userProfile.start.connectedAccountsSection.destructiveActionTitle' ) ,
184
226
isDestructive : true ,
0 commit comments