diff --git a/.changeset/afraid-maps-speak.md b/.changeset/afraid-maps-speak.md
new file mode 100644
index 00000000000..65a64299e09
--- /dev/null
+++ b/.changeset/afraid-maps-speak.md
@@ -0,0 +1,5 @@
+---
+'@aws-amplify/ui-react': patch
+---
+
+fix: Swapped save and cancel buttons.
diff --git a/.changeset/bright-worms-explain.md b/.changeset/bright-worms-explain.md
new file mode 100644
index 00000000000..e8d4e62d7f7
--- /dev/null
+++ b/.changeset/bright-worms-explain.md
@@ -0,0 +1,10 @@
+---
+"@aws-amplify/ui-react-core": patch
+"@aws-amplify/ui-react-native": patch
+"@aws-amplify/ui-react": patch
+"@aws-amplify/ui": patch
+"@aws-amplify/ui-vue": patch
+"@aws-amplify/ui-angular": patch
+---
+
+fix(authenticator): migrate totpSecretCode generation to state machine
diff --git a/.changeset/config.json b/.changeset/config.json
index da31028890b..de2231d05ab 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -10,12 +10,13 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
+ "amplify-ui-angular-mono",
+ "angular-example",
"docs",
+ "e2e",
"environments",
- "angular-example",
"next-example",
- "vue-example",
- "amplify-ui-angular-mono",
- "e2e"
+ "react-native-example",
+ "vue-example"
]
}
diff --git a/.changeset/purple-hotels-tease.md b/.changeset/purple-hotels-tease.md
new file mode 100644
index 00000000000..9a3bd9dd7d6
--- /dev/null
+++ b/.changeset/purple-hotels-tease.md
@@ -0,0 +1,5 @@
+---
+'@aws-amplify/ui-react': patch
+---
+
+fix: Updated error text for max file count to be more explicit.
diff --git a/.changeset/thirty-kangaroos-develop.md b/.changeset/thirty-kangaroos-develop.md
new file mode 100644
index 00000000000..267b7d118c2
--- /dev/null
+++ b/.changeset/thirty-kangaroos-develop.md
@@ -0,0 +1,5 @@
+---
+'@aws-amplify/ui-react': patch
+---
+
+fix: Swap the upload button with the clear all button.
diff --git a/examples/react-native/README.md b/examples/react-native/README.md
index 19c04e8e48a..5d897cdaf25 100644
--- a/examples/react-native/README.md
+++ b/examples/react-native/README.md
@@ -30,13 +30,13 @@ From the monorepo root run the following commands:
yarn && yarn build
```
-1. Install CocoaPod dependencies:
+2. Install CocoaPod dependencies:
```bash
yarn react-native-example ios:pod-install
```
-1. Build and install the Example App:
+3. Build and install the Example App:
```bash
yarn react-native-example ios
@@ -50,7 +50,7 @@ yarn react-native-example ios
yarn && yarn build
```
-1. Build and install the Example App:
+2. Build and install the Example App:
```bash
yarn react-native-example android
@@ -70,19 +70,19 @@ All of the below commands should be run from the monorepo root.
yarn react-native dev
```
-1. To optionally develop against `@aws-amplify/ui`:
+2. To optionally develop against `@aws-amplify/ui`:
```bash
-yarn ui dev
+yarn ui build
```
-1. Run:
+3. Run:
```bash
yarn react-native-example dev
```
-1. Open the app on the iOS simulator or Android emulator.
+4. Open the app on the iOS simulator or Android emulator.
## Storybook
diff --git a/packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html b/packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html
index 4a7c8f7a8cf..b2e16fa3f30 100644
--- a/packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html
+++ b/packages/angular/projects/ui-angular/src/lib/components/authenticator/components/setup-totp/setup-totp.component.html
@@ -17,7 +17,7 @@
{{ this.headerText }}
height="228"
/>
-
{{ secretKey }}
+
{{ totpSecretCode }}
{{ copyTextLabel }}
diff --git a/packages/react/src/components/Storage/FileUploader/UploadPreviewer/index.tsx b/packages/react/src/components/Storage/FileUploader/UploadPreviewer/index.tsx
index 0b5a8a7eaff..8d7c11ce9d4 100644
--- a/packages/react/src/components/Storage/FileUploader/UploadPreviewer/index.tsx
+++ b/packages/react/src/components/Storage/FileUploader/UploadPreviewer/index.tsx
@@ -18,10 +18,11 @@ export function UploadPreviewer({
isLoading,
isSuccessful,
hasMaxFilesError,
+ maxFiles,
onClear,
onFileClick,
}: PreviewerProps): JSX.Element {
- const headingMaxFiles = translate('Over Max files');
+ const headingMaxFiles = `${translate('Cannot choose more than')} ${maxFiles}`;
const getUploadedFilesLength = () =>
fileStatuses.filter((file) => file?.fileState === 'success').length;
@@ -78,6 +79,9 @@ export function UploadPreviewer({
{!isLoading && !isSuccessful && (
<>
+
-
-
>
)}
{isSuccessful && (
diff --git a/packages/react/src/components/Storage/FileUploader/UploadTracker/index.tsx b/packages/react/src/components/Storage/FileUploader/UploadTracker/index.tsx
index 15eb24a7fb6..5aa3fd96ba5 100644
--- a/packages/react/src/components/Storage/FileUploader/UploadTracker/index.tsx
+++ b/packages/react/src/components/Storage/FileUploader/UploadTracker/index.tsx
@@ -79,21 +79,21 @@ export function UploadTracker({
<>
>
);
diff --git a/packages/react/src/components/Storage/FileUploader/__tests__/FileUploader.test.tsx b/packages/react/src/components/Storage/FileUploader/__tests__/FileUploader.test.tsx
index 3a9692ebe0a..eeb3e8d18bb 100644
--- a/packages/react/src/components/Storage/FileUploader/__tests__/FileUploader.test.tsx
+++ b/packages/react/src/components/Storage/FileUploader/__tests__/FileUploader.test.tsx
@@ -361,7 +361,7 @@ describe('File Uploader', () => {
render();
// click pencel icon
- const button = screen.getAllByRole('button')[1];
+ const button = screen.getAllByRole('button')[0];
fireEvent.click(button);
// input file name box
const input = screen.getByLabelText('file name');
@@ -391,7 +391,7 @@ describe('File Uploader', () => {
render();
// click pencel icon
- const button = screen.getAllByRole('button')[1];
+ const button = screen.getAllByRole('button')[0];
fireEvent.click(button);
// input file name box
const input = screen.getByLabelText('file name');
@@ -602,7 +602,7 @@ describe('File Uploader', () => {
render();
- const errorText = await screen.findByText(/Over Max files/);
+ const errorText = await screen.findByText(/Cannot choose more than 1/);
expect(errorText).toBeVisible();
});
diff --git a/packages/react/src/components/Storage/FileUploader/types.ts b/packages/react/src/components/Storage/FileUploader/types.ts
index 54ea670c5bd..1b28d8e9402 100644
--- a/packages/react/src/components/Storage/FileUploader/types.ts
+++ b/packages/react/src/components/Storage/FileUploader/types.ts
@@ -39,6 +39,7 @@ export interface PreviewerProps {
isLoading: boolean;
isSuccessful: boolean;
hasMaxFilesError: boolean;
+ maxFiles: number;
onClear: () => void;
onFileClick: () => void;
}
diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts
index 53283f457c4..ef57b96eafd 100644
--- a/packages/ui/src/helpers/authenticator/facade.ts
+++ b/packages/ui/src/helpers/authenticator/facade.ts
@@ -49,6 +49,7 @@ interface AuthenticatorServiceContextFacade {
isPending: boolean;
route: AuthenticatorRoute;
socialProviders: SocialProvider[];
+ totpSecretCode: string | null;
unverifiedContactMethods: UnverifiedContactMethods;
user: AmplifyUser;
validationErrors: AuthenticatorValidationErrors;
@@ -124,6 +125,7 @@ export const getServiceContextFacade = (
remoteError: error,
unverifiedContactMethods,
validationError: validationErrors,
+ totpSecretCode = null,
} = actorContext;
const { socialProviders } = state.context?.config ?? {};
@@ -153,7 +155,8 @@ export const getServiceContextFacade = (
return 'confirmSignUp';
case actorState?.matches('confirmSignIn'):
return 'confirmSignIn';
- case actorState?.matches('setupTOTP'):
+ case actorState?.matches('setupTOTP.edit'):
+ case actorState?.matches('setupTOTP.submit'):
return 'setupTOTP';
case actorState?.matches('signIn'):
return 'signIn';
@@ -169,6 +172,7 @@ export const getServiceContextFacade = (
return 'verifyUser';
case actorState?.matches('confirmVerifyUser'):
return 'confirmVerifyUser';
+ case actorState?.matches('setupTOTP.getTotpSecretCode'):
case state.matches('signIn.runActor'):
/**
* This route is needed for autoSignIn to capture both the
@@ -207,6 +211,7 @@ export const getServiceContextFacade = (
isPending,
route,
socialProviders,
+ totpSecretCode,
unverifiedContactMethods,
user,
validationErrors,
diff --git a/packages/ui/src/helpers/authenticator/utils.ts b/packages/ui/src/helpers/authenticator/utils.ts
index 7fcf24a78e9..db53f877908 100644
--- a/packages/ui/src/helpers/authenticator/utils.ts
+++ b/packages/ui/src/helpers/authenticator/utils.ts
@@ -98,13 +98,14 @@ export const defaultAuthHubHandler: AuthMachineHubHandler = async (
}
}
break;
- case 'autoSignIn_failure':
+ case 'autoSignIn_failure': {
await waitForAutoSignInState(service);
const currentActorState = getActorState(service.getSnapshot());
if (currentActorState?.matches('autoSignIn')) {
send({ type: 'AUTO_SIGN_IN_FAILURE', data: data.payload.data });
}
break;
+ }
case 'signOut':
case 'tokenRefresh_failure':
if (state.matches('authenticated.idle')) {
@@ -133,6 +134,7 @@ const getHubEventHandler =
*/
export const listenToAuthHub = (
service: AuthInterpreter,
+ // angular passes its own `handler` param
handler: AuthMachineHubHandler = defaultAuthHubHandler
) => {
return Hub.listen(
diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts
index 0063125bd57..87526de7ed0 100644
--- a/packages/ui/src/machines/authenticator/actions.ts
+++ b/packages/ui/src/machines/authenticator/actions.ts
@@ -57,6 +57,10 @@ export const clearValidationError = assign({ validationError: (_) => ({}) });
/**
* "set" actions
*/
+export const setTotpSecretCode = assign({
+ totpSecretCode: (_, event: AuthEvent) => event.data,
+});
+
export const setChallengeName = assign({
challengeName: (_, event: AuthEvent) => event.data?.challengeName,
});
diff --git a/packages/ui/src/machines/authenticator/actors/signIn.ts b/packages/ui/src/machines/authenticator/actors/signIn.ts
index b2bffbfaab0..3687ebb64b5 100644
--- a/packages/ui/src/machines/authenticator/actors/signIn.ts
+++ b/packages/ui/src/machines/authenticator/actors/signIn.ts
@@ -29,6 +29,7 @@ import {
setFieldErrors,
setRemoteError,
setRequiredAttributes,
+ setTotpSecretCode,
setUnverifiedContactMethods,
setUser,
setUsernameAuthAttributes,
@@ -372,9 +373,22 @@ export function signInActor({ services }: SignInMachineOptions) {
},
},
setupTOTP: {
- initial: 'edit',
+ initial: 'getTotpSecretCode',
exit: ['clearFormValues', 'clearError', 'clearTouched'],
states: {
+ getTotpSecretCode: {
+ invoke: {
+ src: 'getTotpSecretCode',
+ onDone: {
+ target: 'edit',
+ actions: 'setTotpSecretCode',
+ },
+ onError: {
+ target: 'edit',
+ actions: 'setRemoteError',
+ },
+ },
+ },
edit: {
entry: 'sendUpdate',
on: {
@@ -498,6 +512,7 @@ export function signInActor({ services }: SignInMachineOptions) {
setCredentials,
setFieldErrors,
setRemoteError,
+ setTotpSecretCode,
setUnverifiedContactMethods,
setUser,
setUsernameAuthAttributes,
@@ -606,6 +621,10 @@ export function signInActor({ services }: SignInMachineOptions) {
return Promise.reject(err);
}
},
+ async getTotpSecretCode(context) {
+ const { user } = context;
+ return Auth.setupTOTP(user);
+ },
async verifyTotpToken(context) {
const { formValues, user } = context;
const { confirmation_code } = formValues;
diff --git a/packages/ui/src/types/authenticator/stateMachine/context.ts b/packages/ui/src/types/authenticator/stateMachine/context.ts
index 70cf3d2e61f..29303e25704 100644
--- a/packages/ui/src/types/authenticator/stateMachine/context.ts
+++ b/packages/ui/src/types/authenticator/stateMachine/context.ts
@@ -76,6 +76,8 @@ interface BaseFormContext {
codeDeliveryDetails?: CodeDeliveryDetails;
/** Default country code for all phone number fields. */
country_code?: string; // TODO: this one should be customizable as well
+ /** TOTP secret code */
+ totpSecretCode?: string;
}
// Actor context types
diff --git a/packages/vue/.eslintrc.cjs b/packages/vue/.eslintrc.cjs
index 23399828ce8..c69d73abf81 100644
--- a/packages/vue/.eslintrc.cjs
+++ b/packages/vue/.eslintrc.cjs
@@ -7,7 +7,6 @@ module.exports = {
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
- '@vue/eslint-config-prettier',
],
parserOptions: {
ecmaVersion: 2020,
diff --git a/packages/vue/src/components/authenticator.vue b/packages/vue/src/components/authenticator.vue
index 3eb70f739d9..95e7c76c2bf 100644
--- a/packages/vue/src/components/authenticator.vue
+++ b/packages/vue/src/components/authenticator.vue
@@ -427,7 +427,10 @@ const hasRouteComponent = computed(() => {
diff --git a/packages/vue/src/components/setup-totp.vue b/packages/vue/src/components/setup-totp.vue
index 71e83f72776..f7e97fb4157 100644
--- a/packages/vue/src/components/setup-totp.vue
+++ b/packages/vue/src/components/setup-totp.vue
@@ -3,18 +3,21 @@ import { onMounted, reactive, computed, ComputedRef, useAttrs, ref } from 'vue';
import { createSharedComposable } from '@vueuse/core';
import QRCode from 'qrcode';
-import { Auth, Logger } from 'aws-amplify';
+import { Logger } from 'aws-amplify';
import {
authenticatorTextUtil,
getActorState,
getFormDataFromEvent,
SignInState,
translate,
+ getTotpCodeURL,
} from '@aws-amplify/ui';
import { useAuth, useAuthenticator } from '../composables/useAuth';
import BaseFormFields from './primitives/base-form-fields.vue';
+const logger = new Logger('SetupTOTP-logger');
+
const useAuthShared = createSharedComposable(useAuthenticator);
const props = useAuthShared();
@@ -26,46 +29,45 @@ const {
value: { context },
} = state;
-const formOverrides = context?.config?.formFields?.setupTOTP;
-const QROR = formOverrides?.['QR'];
-
const actorState = computed(() =>
getActorState(state.value)
) as ComputedRef;
+const { totpSecretCode, user } = actorState.value.context;
+
+const formOverrides = context?.config?.formFields?.setupTOTP;
+const { totpIssuer = 'AWSCognito', totpUsername = user?.username } =
+ formOverrides?.['QR'] ?? {};
+
+const totpCodeURL =
+ typeof totpSecretCode === 'string' && typeof totpUsername === 'string'
+ ? getTotpCodeURL(totpIssuer, totpUsername, totpSecretCode)
+ : null;
-let qrCode = reactive({
+const qrCode = reactive({
qrCodeImageSource: '',
isLoading: true,
});
-let secretKey = ref('');
// Text Util
const { getCopyText, getCopiedText, getBackToSignInText, getConfirmText } =
authenticatorTextUtil;
-let copyTextLabel = ref(getCopyText());
+const copyTextLabel = ref(getCopyText());
function copyText() {
- navigator.clipboard.writeText(secretKey.value);
+ if (typeof totpSecretCode === 'string') {
+ navigator.clipboard.writeText(totpSecretCode);
+ }
copyTextLabel.value = getCopiedText();
}
// lifecycle hooks
-
onMounted(async () => {
- const logger = new Logger('SetupTOTP-logger');
- const { user } = actorState.value.context;
- if (!user) {
+ if (!user || !totpCodeURL) {
return;
}
try {
- secretKey.value = await Auth.setupTOTP(user);
- const issuer = QROR?.totpIssuer ?? 'AWSCognito';
- const username = QROR?.totpUsername ?? user.username;
- const totpCode = encodeURI(
- `otpauth://totp/${issuer}:${username}?secret=${secretKey.value}&issuer=${issuer}`
- );
- qrCode.qrCodeImageSource = await QRCode.toDataURL(totpCode);
+ qrCode.qrCodeImageSource = await QRCode.toDataURL(totpCodeURL);
} catch (error) {
logger.error(error);
} finally {
@@ -80,11 +82,7 @@ const confirmText = computed(() => getConfirmText());
// Methods
const onInput = (e: Event): void => {
const { name, value } = e.target as HTMLInputElement;
- send({
- type: 'CHANGE',
- //@ts-ignore
- data: { name, value },
- });
+ send({ type: 'CHANGE', data: { name, value } });
};
const onSetupTOTPSubmit = (e: Event): void => {
@@ -103,9 +101,7 @@ const onBackToSignInClicked = (): void => {
if (attrs?.onBackToSignInClicked) {
emit('backToSignInClicked');
} else {
- send({
- type: 'SIGN_IN',
- });
+ send({ type: 'SIGN_IN' });
}
};
@@ -122,18 +118,18 @@ const onBackToSignInClicked = (): void => {
class="amplify-flex amplify-authenticator__column"
:disabled="actorState.matches('confirmSignIn.pending')"
>
-
- Loading...
-
-
-
-
-
- Setup TOTP
-
-
+
+
+
+ Setup TOTP
+
+
-
+
+
+ Loading...
+
+
{
width="228"
height="228"
/>
-
- {{ secretKey }}
-
- {{ copyTextLabel }}
-
-
-
-
+
+
+ {{ totpSecretCode }}
+
+ {{ copyTextLabel }}
+
+
+
-
-
-
- {{ translate(actorState.context.remoteError) }}
-
-
- {{ confirmText }}
-
-
- {{ backSignInText }}
-
-
-
+
-
+
+
+ {{ translate(actorState.context.remoteError) }}
+
+
+ {{ confirmText }}
+
+
+ {{ backSignInText }}
+
+
+
+