Skip to content

Commit ed6bdf7

Browse files
committed
Add an option to copy the client secret of confidential apps
1 parent 6fb0a68 commit ed6bdf7

13 files changed

Lines changed: 1298 additions & 28 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {Box, Typography, Stack, TextField, IconButton, InputAdornment, Alert, Button, Divider} from '@wso2/oxygen-ui';
20+
import {Copy, Eye, EyeOff, AlertTriangle} from '@wso2/oxygen-ui-icons-react';
21+
import type {JSX} from 'react';
22+
import {useState} from 'react';
23+
import {useTranslation} from 'react-i18next';
24+
import {useCopyToClipboard} from '@thunder/shared-hooks';
25+
26+
export interface ShowClientSecretProps {
27+
/**
28+
* The name of the created application
29+
*/
30+
appName: string;
31+
/**
32+
* The client secret that needs to be saved
33+
*/
34+
clientSecret: string;
35+
/**
36+
* Callback when user clicks copy secret button
37+
*/
38+
onCopySecret?: () => void;
39+
/**
40+
* Callback when user clicks continue button
41+
*/
42+
onContinue: () => void;
43+
}
44+
45+
/**
46+
* Component that displays the client secret that needs to be saved
47+
* with security reminders and educational content
48+
*/
49+
export default function ShowClientSecret({
50+
appName,
51+
clientSecret,
52+
onCopySecret = () => {},
53+
onContinue,
54+
}: ShowClientSecretProps): JSX.Element {
55+
const {t} = useTranslation();
56+
const [showSecret, setShowSecret] = useState(false);
57+
const {copied, copy} = useCopyToClipboard({
58+
resetDelay: 2000,
59+
onCopy: onCopySecret,
60+
}) as {copied: boolean; copy: (text: string) => Promise<void>};
61+
62+
const handleCopy = async (): Promise<void> => {
63+
await copy(clientSecret);
64+
};
65+
66+
const handleToggleVisibility = (): void => {
67+
setShowSecret(!showSecret);
68+
};
69+
70+
return (
71+
<Stack direction="column" spacing={4} sx={{width: '100%'}}>
72+
{/* Warning Icon */}
73+
<Box
74+
sx={{
75+
width: 64,
76+
height: 64,
77+
borderRadius: 2,
78+
display: 'flex',
79+
alignItems: 'center',
80+
justifyContent: 'center',
81+
alignSelf: 'center',
82+
}}
83+
>
84+
<AlertTriangle size={64} color="var(--mui-palette-warning-main)" />
85+
</Box>
86+
87+
{/* Header */}
88+
<Stack direction="column" spacing={1} sx={{textAlign: 'center'}}>
89+
<Typography variant="h3" component="h1">
90+
{t('applications:clientSecret.saveTitle')}
91+
</Typography>
92+
<Typography variant="body1" color="text.secondary">
93+
{t('applications:clientSecret.saveSubtitle')}
94+
</Typography>
95+
</Stack>
96+
97+
{/* Application Name & Client Secret Card */}
98+
<Box
99+
sx={{
100+
p: 3,
101+
bgcolor: 'background.paper',
102+
border: '1px solid',
103+
borderColor: 'divider',
104+
borderRadius: 1,
105+
}}
106+
>
107+
<Stack direction="column" spacing={2}>
108+
<Box>
109+
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 0.5}}>
110+
{t('applications:clientSecret.appNameLabel')}
111+
</Typography>
112+
<Typography variant="body1">{appName}</Typography>
113+
</Box>
114+
115+
<Divider />
116+
117+
<Box>
118+
<Typography variant="caption" color="text.secondary" sx={{display: 'block', mb: 1}}>
119+
{t('applications:clientSecret.clientSecretLabel')}
120+
</Typography>
121+
<TextField
122+
fullWidth
123+
type={showSecret ? 'text' : 'password'}
124+
value={clientSecret}
125+
InputProps={{
126+
readOnly: true,
127+
endAdornment: (
128+
<InputAdornment position="end">
129+
<IconButton onClick={handleToggleVisibility} edge="end" size="small">
130+
{showSecret ? <EyeOff size={16} /> : <Eye size={16} />}
131+
</IconButton>
132+
<IconButton
133+
onClick={() => {
134+
handleCopy().catch(() => {
135+
// Error already handled in handleCopy
136+
});
137+
}}
138+
edge="end"
139+
size="small"
140+
sx={{ml: 0.5}}
141+
>
142+
<Copy size={16} />
143+
</IconButton>
144+
</InputAdornment>
145+
),
146+
}}
147+
/>
148+
</Box>
149+
</Stack>
150+
</Box>
151+
152+
{/* Security Reminder Alert */}
153+
<Alert severity="warning" icon={<AlertTriangle size={20} />}>
154+
<Typography variant="body2" sx={{fontWeight: 'medium', mb: 1}}>
155+
{t('applications:clientSecret.securityReminder.title')}
156+
</Typography>
157+
<Typography variant="body2">{t('applications:clientSecret.securityReminder.description')}</Typography>
158+
</Alert>
159+
160+
{/* Action Buttons */}
161+
<Stack direction="row" spacing={2} sx={{width: '100%'}}>
162+
<Button
163+
variant="contained"
164+
fullWidth
165+
startIcon={<Copy size={16} />}
166+
onClick={() => {
167+
handleCopy().catch(() => {
168+
// Error already handled in handleCopy
169+
});
170+
}}
171+
disabled={copied}
172+
>
173+
{copied ? t('applications:clientSecret.copied') : t('applications:clientSecret.copySecret')}
174+
</Button>
175+
<Button variant="outlined" fullWidth onClick={onContinue}>
176+
{t('common:actions.continue')}
177+
</Button>
178+
</Stack>
179+
</Stack>
180+
);
181+
}

0 commit comments

Comments
 (0)