Skip to content

Commit 2e7dbae

Browse files
ErikCHErik Hanchett
and
Erik Hanchett
authored
Updated to QR code page (#1083)
Co-authored-by: Erik Hanchett <ehhanche@amazon.com>
1 parent 4ac3cef commit 2e7dbae

File tree

6 files changed

+203
-91
lines changed

6 files changed

+203
-91
lines changed

Diff for: .changeset/big-emus-brake.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@aws-amplify/ui-angular': patch
3+
'@aws-amplify/ui-react': patch
4+
'@aws-amplify/ui-vue': patch
5+
---
6+
7+
Updated QR code page so users on mobile don't have to take a picture of the QR code

Diff for: packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html

+19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ <h3 class="amplify-heading">{{ this.headerText }}</h3>
1818
width="228"
1919
height="228"
2020
/>
21+
<div class="amplify-flex" data-amplify-copy>
22+
<div>{{ secretKey }}</div>
23+
<div data-amplify-copy-svg (click)="copyText()">
24+
<div data-amplify-copy-tooltip>{{ copyTextLabel }}</div>
25+
<svg
26+
width="24"
27+
height="24"
28+
viewBox="0 0 24 24"
29+
fill="none"
30+
xmlns="http://www.w3.org/2000/svg"
31+
>
32+
<path
33+
d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM15 5H8C6.9 5 6.01 5.9 6.01 7L6 21C6 22.1 6.89 23 7.99 23H19C20.1 23 21 22.1 21 21V11L15 5ZM8 21V7H14V12H19V21H8Z"
34+
fill="black"
35+
/>
36+
</svg>
37+
</div>
38+
</div>
39+
2140
<amplify-form-field
2241
name="confirmation_code"
2342
label="Code *"

Diff for: packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class SetupTotpComponent implements OnInit {
1515
@HostBinding('attr.data-amplify-authenticator-setup-totp') dataAttr = '';
1616
public headerText = translate('Setup TOTP');
1717
public qrCodeSource = '';
18+
public secretKey = '';
19+
public copyTextLabel = translate('COPY');
1820

1921
// translated texts
2022
public backToSignInText = translate('Back to Sign In');
@@ -36,9 +38,9 @@ export class SetupTotpComponent implements OnInit {
3638
const actorContext = getActorContext(state) as SignInContext;
3739
const { user } = actorContext;
3840
try {
39-
const secretKey = await Auth.setupTOTP(user);
41+
this.secretKey = await Auth.setupTOTP(user);
4042
const issuer = 'AWSCognito';
41-
const totpCode = `otpauth://totp/${issuer}:${user.username}?secret=${secretKey}&issuer=${issuer}`;
43+
const totpCode = `otpauth://totp/${issuer}:${user.username}?secret=${this.secretKey}&issuer=${issuer}`;
4244

4345
logger.info('totp code was generated:', totpCode);
4446
this.qrCodeSource = await QRCode.toDataURL(totpCode);
@@ -57,4 +59,9 @@ export class SetupTotpComponent implements OnInit {
5759
event.preventDefault();
5860
this.authenticator.submitForm();
5961
}
62+
63+
copyText(): void {
64+
navigator.clipboard.writeText(this.secretKey);
65+
this.copyTextLabel = translate('COPIED');
66+
}
6067
}

Diff for: packages/react/src/components/Authenticator/SetupTOTP/SetupTOTP.tsx

+26-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const logger = new Logger('SetupTOTP-logger');
1919
export const SetupTOTP = (): JSX.Element => {
2020
const [isLoading, setIsLoading] = useState<boolean>(true);
2121
const [qrCode, setQrCode] = useState<string>();
22+
const [copyTextLabel, setCopyTextLabel] = useState<string>('COPY');
23+
const [secretKey, setSecretKey] = useState<string>('');
2224
const { _state, submitForm, updateForm } = useAuthenticator();
2325

2426
// `user` hasn't been set on the top-level state yet, so it's only available from the signIn actor
@@ -27,7 +29,7 @@ export const SetupTOTP = (): JSX.Element => {
2729

2830
const generateQRCode = async (user): Promise<void> => {
2931
try {
30-
const secretKey = await Auth.setupTOTP(user);
32+
setSecretKey(await Auth.setupTOTP(user));
3133
const issuer = 'AWSCognito';
3234
const totpCode = `otpauth://totp/${issuer}:${user.username}?secret=${secretKey}&issuer=${issuer}`;
3335
const qrCodeImageSource = await QRCode.toDataURL(totpCode);
@@ -66,6 +68,11 @@ export const SetupTOTP = (): JSX.Element => {
6668
submitForm();
6769
};
6870

71+
const copyText = (): void => {
72+
navigator.clipboard.writeText(secretKey);
73+
setCopyTextLabel(translate('COPIED'));
74+
};
75+
6976
return (
7077
<form
7178
data-amplify-form=""
@@ -90,6 +97,24 @@ export const SetupTOTP = (): JSX.Element => {
9097
height="228"
9198
/>
9299
)}
100+
<Flex data-amplify-copy>
101+
<div>{secretKey}</div>
102+
<Flex data-amplify-copy-svg onClick={copyText}>
103+
<div data-amplify-copy-tooltip>{copyTextLabel}</div>
104+
<svg
105+
width="24"
106+
height="24"
107+
viewBox="0 0 24 24"
108+
fill="none"
109+
xmlns="http://www.w3.org/2000/svg"
110+
>
111+
<path
112+
d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM15 5H8C6.9 5 6.01 5.9 6.01 7L6 21C6 22.1 6.89 23 7.99 23H19C20.1 23 21 22.1 21 21V11L15 5ZM8 21V7H14V12H19V21H8Z"
113+
fill="black"
114+
/>
115+
</svg>
116+
</Flex>
117+
</Flex>
93118
<ConfirmationCodeInput />
94119
<RemoteErrorMessage />
95120
</Flex>

Diff for: packages/ui/src/theme/css/component/authenticator.scss

+29
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,32 @@
139139
padding: 0px 1.5rem;
140140
}
141141
}
142+
[data-amplify-copy] {
143+
display: flex;
144+
font-size: 0.688rem;
145+
gap: 1rem;
146+
justify-content: center;
147+
align-items: center;
148+
149+
@media (max-width: 30rem) {
150+
word-break: break-all;
151+
}
152+
}
153+
154+
[data-amplify-copy-svg] {
155+
cursor: pointer;
156+
position: relative;
157+
158+
&:hover [data-amplify-copy-tooltip] {
159+
visibility: visible;
160+
font-size: 0.55rem;
161+
}
162+
}
163+
164+
[data-amplify-copy-tooltip] {
165+
visibility: hidden;
166+
position: absolute;
167+
top: -1rem;
168+
left: -0.1rem;
169+
color: var(--amplify-colors-teal-100);
170+
}

Diff for: packages/vue/src/components/setup-totp.vue

+113-88
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,98 @@
1+
<script setup lang="ts">
2+
import { onMounted, reactive, computed, ComputedRef, useAttrs, ref } from 'vue';
3+
import QRCode from 'qrcode';
4+
5+
import { Auth, Logger } from 'aws-amplify';
6+
import { getActorState, SignInState, translate } from '@aws-amplify/ui';
7+
8+
import { useAuth } from '../composables/useAuth';
9+
10+
const attrs = useAttrs();
11+
const emit = defineEmits(['confirmSetupTOTPSubmit', 'backToSignInClicked']);
12+
13+
const { state, send } = useAuth();
14+
const actorState = computed(() =>
15+
getActorState(state.value)
16+
) as ComputedRef<SignInState>;
17+
18+
let qrCode = reactive({
19+
qrCodeImageSource: '',
20+
isLoading: true,
21+
});
22+
let secretKey = ref('');
23+
let copyTextLabel = ref(translate('COPY'));
24+
25+
function copyText() {
26+
navigator.clipboard.writeText(secretKey.value);
27+
copyTextLabel.value = translate('COPIED');
28+
}
29+
30+
// lifecycle hooks
31+
32+
onMounted(async () => {
33+
const logger = new Logger('SetupTOTP-logger');
34+
const { user } = actorState.value.context;
35+
if (!user) {
36+
return;
37+
}
38+
try {
39+
secretKey.value = await Auth.setupTOTP(user);
40+
const issuer = 'AWSCognito';
41+
const totpCode = `otpauth://totp/${issuer}:${user.username}?secret=${secretKey}&issuer=${issuer}`;
42+
qrCode.qrCodeImageSource = await QRCode.toDataURL(totpCode);
43+
} catch (error) {
44+
logger.error(error);
45+
} finally {
46+
qrCode.isLoading = false;
47+
}
48+
});
49+
50+
// Computed Properties
51+
const backSignInText = computed(() => translate('Back to Sign In'));
52+
const confirmText = computed(() => translate('Confirm'));
53+
const codeText = computed(() => translate('Code'));
54+
55+
// Methods
56+
const onInput = (e: Event): void => {
57+
const { name, value } = <HTMLInputElement>e.target;
58+
send({
59+
type: 'CHANGE',
60+
//@ts-ignore
61+
data: { name, value },
62+
});
63+
};
64+
65+
const onSetupTOTPSubmit = (e: Event): void => {
66+
if (attrs?.onConfirmSetupTOTPSubmit) {
67+
emit('confirmSetupTOTPSubmit', e);
68+
} else {
69+
submit(e);
70+
}
71+
};
72+
73+
const submit = (e: Event): void => {
74+
const formData = new FormData(<HTMLFormElement>e.target);
75+
send({
76+
type: 'SUBMIT',
77+
//@ts-ignore
78+
data: {
79+
//@ts-ignore
80+
...Object.fromEntries(formData),
81+
},
82+
});
83+
};
84+
85+
const onBackToSignInClicked = (): void => {
86+
if (attrs?.onBackToSignInClicked) {
87+
emit('backToSignInClicked');
88+
} else {
89+
send({
90+
type: 'SIGN_IN',
91+
});
92+
}
93+
};
94+
</script>
95+
196
<template>
297
<slot v-bind="$attrs" name="confirmSetupTOTPI">
398
<base-wrapper v-bind="$attrs">
@@ -31,6 +126,24 @@
31126
width="228"
32127
height="228"
33128
/>
129+
<base-wrapper class="amplify-flex" data-amplify-copy>
130+
<div>{{ secretKey }}</div>
131+
<base-wrapper data-amplify-copy-svg @click="copyText">
132+
<div data-amplify-copy-tooltip>{{ copyTextLabel }}</div>
133+
<svg
134+
width="24"
135+
height="24"
136+
viewBox="0 0 24 24"
137+
fill="none"
138+
xmlns="http://www.w3.org/2000/svg"
139+
>
140+
<path
141+
d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM15 5H8C6.9 5 6.01 5.9 6.01 7L6 21C6 22.1 6.89 23 7.99 23H19C20.1 23 21 22.1 21 21V11L15 5ZM8 21V7H14V12H19V21H8Z"
142+
fill="black"
143+
/>
144+
</svg>
145+
</base-wrapper>
146+
</base-wrapper>
34147
<base-wrapper
35148
class="amplify-flex amplify-field amplify-textfield"
36149
style="flex-direction: column"
@@ -94,91 +207,3 @@
94207
</base-wrapper>
95208
</slot>
96209
</template>
97-
98-
<script setup lang="ts">
99-
import { onMounted, reactive, computed, ComputedRef, useAttrs } from 'vue';
100-
import QRCode from 'qrcode';
101-
102-
import { Auth, Logger } from 'aws-amplify';
103-
import { getActorState, SignInState, translate } from '@aws-amplify/ui';
104-
105-
import { useAuth } from '../composables/useAuth';
106-
107-
const attrs = useAttrs();
108-
const emit = defineEmits(['confirmSetupTOTPSubmit', 'backToSignInClicked']);
109-
110-
const { state, send } = useAuth();
111-
const actorState = computed(() =>
112-
getActorState(state.value)
113-
) as ComputedRef<SignInState>;
114-
115-
let qrCode = reactive({
116-
qrCodeImageSource: '',
117-
isLoading: true,
118-
});
119-
120-
// lifecycle hooks
121-
122-
onMounted(async () => {
123-
const logger = new Logger('SetupTOTP-logger');
124-
const { user } = actorState.value.context;
125-
if (!user) {
126-
return;
127-
}
128-
try {
129-
const secretKey = await Auth.setupTOTP(user);
130-
const issuer = 'AWSCognito';
131-
const totpCode = `otpauth://totp/${issuer}:${user.username}?secret=${secretKey}&issuer=${issuer}`;
132-
qrCode.qrCodeImageSource = await QRCode.toDataURL(totpCode);
133-
} catch (error) {
134-
logger.error(error);
135-
} finally {
136-
qrCode.isLoading = false;
137-
}
138-
});
139-
140-
// Computed Properties
141-
const backSignInText = computed(() => translate('Back to Sign In'));
142-
const confirmText = computed(() => translate('Confirm'));
143-
const codeText = computed(() => translate('Code'));
144-
145-
// Methods
146-
const onInput = (e: Event): void => {
147-
const { name, value } = <HTMLInputElement>e.target;
148-
send({
149-
type: 'CHANGE',
150-
//@ts-ignore
151-
data: { name, value },
152-
});
153-
};
154-
155-
const onSetupTOTPSubmit = (e: Event): void => {
156-
if (attrs?.onConfirmSetupTOTPSubmit) {
157-
emit('confirmSetupTOTPSubmit', e);
158-
} else {
159-
submit(e);
160-
}
161-
};
162-
163-
const submit = (e: Event): void => {
164-
const formData = new FormData(<HTMLFormElement>e.target);
165-
send({
166-
type: 'SUBMIT',
167-
//@ts-ignore
168-
data: {
169-
//@ts-ignore
170-
...Object.fromEntries(formData),
171-
},
172-
});
173-
};
174-
175-
const onBackToSignInClicked = (): void => {
176-
if (attrs?.onBackToSignInClicked) {
177-
emit('backToSignInClicked');
178-
} else {
179-
send({
180-
type: 'SIGN_IN',
181-
});
182-
}
183-
};
184-
</script>

0 commit comments

Comments
 (0)