diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3d807f47fcf..ff7709b009a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,3 @@ -* @aws-amplify/documentation-team - #Other /src/pages/gen1/\[platform\]/build-a-backend/existing-resources/ @josefaidt @hdworld11 @dbanksdesign /src/pages/gen1/\[platform\]/build-a-backend/functions/ @josefaidt @@ -162,9 +160,9 @@ /src/pages/gen1/\[platform\]/prev/build-a-backend/utilities/ @josefaidt #Deploy and Host -/src/pages/gen1/\[platform\]/deploy-and-host/ @mauerbac -/src/pages/gen1/\[platform\]/prev/deploy-and-host/ @mauerbac -/src/pages/\[platform\]/deploy-and-host/ @mauerbac +/src/pages/gen1/\[platform\]/deploy-and-host/ @mauerbac @josefaidt +/src/pages/gen1/\[platform\]/prev/deploy-and-host/ @mauerbac @josefaidt +/src/pages/\[platform\]/deploy-and-host/ @mauerbac @josefaidt #Docs Engineering /src/components @aws-amplify/documentation-team @@ -186,4 +184,4 @@ .github @aws-amplify/documentation-team #Protected Content -/src/protected @reesscot @srquinn21 @swaminator +/src/protected @srquinn21 @swaminator diff --git a/.github/workflows/accessibility_scan.yml b/.github/workflows/accessibility_scan.yml index 793b0471cf6..86f641d3ad7 100644 --- a/.github/workflows/accessibility_scan.yml +++ b/.github/workflows/accessibility_scan.yml @@ -8,12 +8,12 @@ env: jobs: accessibility: name: Runs accessibility scan on changed pages - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout branch uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install dependencies @@ -32,7 +32,7 @@ jobs: const buildDir = process.env.BUILD_DIR; return getChangedPages({github, context, buildDir}); - name: Run site - run: | + run: | python -m http.server 3000 -d ${{ env.BUILD_DIR }} & sleep 5 - name: Run accessibility tests on changed/new MDX pages diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c118fac30d5..2de123358fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20.x - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies diff --git a/.github/workflows/check_bundle_size.yml b/.github/workflows/check_bundle_size.yml index 7a7accfadbe..c5082c74bdc 100644 --- a/.github/workflows/check_bundle_size.yml +++ b/.github/workflows/check_bundle_size.yml @@ -14,7 +14,7 @@ jobs: with: ref: main - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install dependencies @@ -38,7 +38,7 @@ jobs: with: ref: ${{ github.head_ref }} - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install dependencies diff --git a/.github/workflows/check_for_broken_links.yml b/.github/workflows/check_for_broken_links.yml index 45aa18f9cde..47b54fb1ca7 100644 --- a/.github/workflows/check_for_broken_links.yml +++ b/.github/workflows/check_for_broken_links.yml @@ -8,12 +8,12 @@ permissions: id-token: write jobs: LinkChecker: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies @@ -27,7 +27,7 @@ jobs: const { checkProdLinks } = require('./tasks/link-checker.js'); return await checkProdLinks(); - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + uses: aws-actions/configure-aws-credentials@4fc4975a852c8cd99761e2de1f4ba73402e44dd9 # v4.0.3 with: role-to-assume: arn:aws:iam::464149486631:role/github_action_read_slack_webhook_url aws-region: us-west-2 diff --git a/.github/workflows/check_for_console_errors.yml b/.github/workflows/check_for_console_errors.yml index d103ab66a73..f60a0e0bc93 100644 --- a/.github/workflows/check_for_console_errors.yml +++ b/.github/workflows/check_for_console_errors.yml @@ -9,12 +9,12 @@ permissions: contents: read jobs: CheckConsoleErrors: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20.x - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies diff --git a/.github/workflows/check_for_deleted_assets.yml b/.github/workflows/check_for_deleted_assets.yml index d6c7f5beca8..dec78e79a8d 100644 --- a/.github/workflows/check_for_deleted_assets.yml +++ b/.github/workflows/check_for_deleted_assets.yml @@ -32,7 +32,7 @@ jobs: echo ${{ env.PR_NUMBER }} >> $artifactName echo ${{ steps.set-deleted-files-count.outputs.result }} >> $artifactName - name: Upload the deleted assets file to artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ env.ARTIFACT_NAME }} path: '${{ env.ARTIFACT_NAME }}.txt' diff --git a/.github/workflows/check_for_redirects.yml b/.github/workflows/check_for_redirects.yml index d4b244c5f41..bf94105f613 100644 --- a/.github/workflows/check_for_redirects.yml +++ b/.github/workflows/check_for_redirects.yml @@ -32,7 +32,7 @@ jobs: echo ${{ env.PR_NUMBER }} >> $artifactName echo ${{ steps.set-deleted-files-count.outputs.result }} >> $artifactName - name: Upload the redirects file to artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ env.ARTIFACT_NAME }} path: '${{ env.ARTIFACT_NAME }}.txt' diff --git a/.github/workflows/check_pr_for_broken_links.yml b/.github/workflows/check_pr_for_broken_links.yml index 766801b41da..e5d3cddc829 100644 --- a/.github/workflows/check_pr_for_broken_links.yml +++ b/.github/workflows/check_pr_for_broken_links.yml @@ -7,12 +7,12 @@ env: BUILD_DIR: 'client/www/next-build' jobs: CheckPRLinks: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index e8ce1664edb..7b624d438c5 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -11,7 +11,7 @@ jobs: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies diff --git a/.github/workflows/update_references.yml b/.github/workflows/update_references.yml index 8158796633c..8640d19dea1 100644 --- a/.github/workflows/update_references.yml +++ b/.github/workflows/update_references.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x diff --git a/.github/workflows/validate_redirects.yml b/.github/workflows/validate_redirects.yml index 8affcb4b074..05c71cf18a3 100644 --- a/.github/workflows/validate_redirects.yml +++ b/.github/workflows/validate_redirects.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repository uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Node.js 20.x - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20.x - name: Install Dependencies diff --git a/Readme.md b/Readme.md index 4610e6201fc..60975e0a178 100644 --- a/Readme.md +++ b/Readme.md @@ -37,7 +37,7 @@ We welcome contributions to the documentation site! Here's how to do it: ## Authoring pages -Our docs are generated using [Next.js](https://nextjs.org/). Refer to their docs on [how to create pages](https://nextjs.org/docs/basic-features/pages) as a primer. +Our docs are generated using [Next.js](https://nextjs.org/). Refer to their docs on [how to create pages](https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts) as a primer. The source for each page is in **src**. This folder is the only directory you need to touch to edit or create pages. diff --git a/cspell.json b/cspell.json index abb83dd527d..d4d087165c3 100644 --- a/cspell.json +++ b/cspell.json @@ -68,6 +68,7 @@ "amazonaws", "amazonaws", "amazoncognito", + "amazonlinux", "AmazonPersonalizeProvider", "AmazonS3Client", "Amplif", @@ -246,6 +247,7 @@ "aws-sdk-ios", "aws.cognito.signin.user.admin", "aws", + "Authadmin", "AWSAPI", "AWSAPIGateway", "AWSAPIPlugin", @@ -494,6 +496,7 @@ "dataaccess", "dataacess", "databinding", + "datalogconfig", "dataset", "datasource", "DataSource", @@ -1614,14 +1617,29 @@ "ampx", "autodetection", "jamba", - "knowledgebases" + "webauthn", + "knowledgebases", + "rehype", + "assetlinks", + "AMPLIFYRULES", + "manylinux", + "GOARCH", + "norpc" + ], + "flagWords": [ + "hte", + "full-stack", + "Full-stack", + "Full-Stack", + "sudo" ], - "flagWords": ["hte", "full-stack", "Full-stack", "Full-Stack", "sudo"], "patterns": [ { "name": "youtube-embed-ids", "pattern": "/embedId=\".*\" /" } ], - "ignoreRegExpList": ["youtube-embed-ids"] + "ignoreRegExpList": [ + "youtube-embed-ids" + ] } diff --git a/mdx-components.tsx b/mdx-components.tsx index a587becd6b3..7ce3e403259 100644 --- a/mdx-components.tsx +++ b/mdx-components.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import type { MDXComponents } from 'mdx/types'; import ExportedImage from 'next-image-export-optimizer'; +import { AIBanner } from './src/components/AIBanner'; import InlineFilter from './src/components/InlineFilter'; import { YoutubeEmbed } from './src/components/YoutubeEmbed'; import { Accordion } from './src/components/Accordion'; @@ -64,6 +65,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { InlineFilter, MigrationAlert, YoutubeEmbed, + AIBanner, Overview, ExternalLink, ExternalLinkButton, diff --git a/package.json b/package.json index 3a03e6f7ac2..587f6c10171 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "private": true, "dependencies": { "@aws-amplify/amplify-cli-core": "^4.3.9", - "@aws-amplify/ui-react": "^6.3.1", + "@aws-amplify/ui-react": "^6.7.0", + "@aws-amplify/ui-react-ai": "^1.0.0", "@docsearch/react": "3", "ajv": "^8.16.0", "aws-amplify": "^6.0.9", @@ -89,6 +90,7 @@ "minimatch": "3.1.2", "decode-uri-component": "0.2.1", "**/fast-xml-parser": "4.4.1", + "cross-spawn": "^7.0.5", "semver": "7.5.2", "tough-cookie": "4.1.3", "aws-cdk-lib": "2.80.0", @@ -118,5 +120,6 @@ "analyze": "ANALYZE=true yarn next-build", "prebuild": "node src/directory/generateDirectory.mjs && node src/directory/generateFlatDirectory.mjs", "lint": "next lint" - } + }, + "packageManager": "yarn@1.22.22" } diff --git a/public/images/gen2/account-setup/profile4.png b/public/images/gen2/account-setup/profile4.png deleted file mode 100644 index d2543460fa2..00000000000 Binary files a/public/images/gen2/account-setup/profile4.png and /dev/null differ diff --git a/public/images/gen2/q-developer/authentication.md b/public/images/gen2/q-developer/authentication.md new file mode 100644 index 00000000000..2ec836d9794 --- /dev/null +++ b/public/images/gen2/q-developer/authentication.md @@ -0,0 +1,219 @@ +# AMPLIFYRULES + +- RULES THAT SHOULD BE ADHERED TO THE LAST WORD. + + 1. EXTERNAL PROVIDERS THAT ARE AVAILABLE ARE LISTED BELOW IN THE EXAMPLE, DON'T CHANGE THE NAMING CONVENTION WHILE USING THOSE IN THE CODE GENERATION. + 2. DON'T FORGET TO IMPORT SECRET FOR ANY AUTHENTICATION BASED QUESTION. + + ```typescript + import { defineAuth, secret } from "@aws-amplify/backend"; + ``` + + 3. CALLBACK AND LOGOUT URLS SHOULD BE INSIDE THE "EXTERNALPROVIDERS" OBJECT. + 4. WHILE ADDING THE CUSTOM ATTRIBUTES, IF THE ATTRIBUTE YOU ARE ADDING DOESNT BELONG TO THE STANDARD USER ATTRIBUTES LIST THEN ADD IT AS A CUSTOM ATTRIBUTE LIKE THIS "CUSTOM:ATTRIBUTE_NAME" AND THIS DOESN'T SUPPORT "REQUIRED" FIELD SO IGNORE IT WHILE GENERATING THE ANSWER. + 5. WHILE ADDING THE CUSTOM ATTRIBUTES, MAKE SURE TO ALWAYS ADD THE "DATATYPE" FIELD AS IT IS A REQUIRED FIELD. + 6. STATNDARD ATTIBUTES THAT ARE ALLOWED: `familyName`, `giveName`, `middleName`, `nickname`, `preferredUsername`, `profile`, `profilePicture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale`, `updatedAt`, `address`, `email`, `phoneNumber`, `sub`. THE `userAttributes` ARE SUPPOSED TO BE OUTSIDE THE `loginWith` OBJECT + + 7. THE FOLLOWING IS THE REQUIRED SYNTAX FOR `externalProviders`. ONLY THE FOUR LISTED PROVIDERS BELOW ARE SUPPORTED: + + ```typescript + loginWith:{ + //loginMethods + externalProviders: { + google: { + + }, + signInWithApple: { + }, + loginWithAmazon: { + + }, + facebook: { + }, + callbackUrls: [ + // Callback URLs should be included inside the `externalProviders` object only, as per rule. + + ], + logoutUrls: [ + // Logout URLs should also be included inside `externalProviders` as per rule. + + ], + }, + } + ``` + + 8. THE `userAttributes` ARE SUPPOSED TO BE OUTSIDE THE `loginWith` OBJECT + `// Example configuration for user attributes and login methods +loginWith: { + // Specify login methods separately from user attributes, ONLY MENTION THE LOGINMETHODS THAT ARE SUPPOSED TO BE TRUE + email: true, // Enable login with email + phone: false, // Disable login with phone number +},userAttributes: { + email: true, // Attribute set as required + phoneNumber: false, // Attribute not required + givenName: true, // Attribute set as required +},` + 9. THIS IS THE WAY TO SETUP THE MULTIFACTOR AUTHENTHICATION + export const auth = defineAuth({ + loginWith: { + email: true, + phone: true, + }, + multifactor: { + mode: "OPTIONAL", + sms: true, + totp: false, + }, + // IMPORTANT! THE LOGIC TO RESOLVE THIS VALUE CANNOT DETERMINE WHETHER EMAIL MFA IS ENABLED WHEN OVERRIDING THE RESOURCE. + // BE SURE TO PICK A RECOVERY OPTION APPROPRIATE FOR YOUR APPLICATION. + accountRecovery: "EMAIL_AND_PHONE_WITHOUT_MFA", + senders: { + email: { + fromEmail: "registrations@example.com", + }, + }, + }) + 10. DON'T ADD THE `passwordPolicy` IN THE AUTH/RESOURCE.TS FILE. + 11. THE `username` SHOULD BE REPLACED WITH `userAttributes` WHEN REQUESTED TO SETUP USERNAME AS A LOGIN METHOD AS USERNAME IS NOT A VALID LOGIN METHOD ON DEFINEAUTH, FOR EXAMPLE: + + ```typescript + userAttributes: { + preferredUsername: { + mutable: true, + required: false + } + } + ``` + + 12. `loginWith` SUPPORTS ONLY TWO METHODS THAT IS `email` and `phone`. THERE IS NO `username` attribute to that. + 13. THE `callbackUrls` AND `logoutUrls` SHOULD ONLY BE MENTIONED ONCE AS MENTIONED IN RULE #7 AND NOT FOR EACH EXTERNAL PROVIDER + +```typescript +import { defineAuth, secret } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + // Login Methods Configuration + loginWith: { + // Only email and phone are supported as login methods + email: true, + phone: true, + + // External Providers Configuration - all providers shown with required fields + externalProviders: { + // Google Authentication + google: { + clientId: secret("GOOGLE_CLIENT_ID"), + clientSecret: secret("GOOGLE_CLIENT_SECRET"), + }, + // Sign in with Apple + signInWithApple: { + clientId: secret("SIWA_CLIENT_ID"), + keyId: secret("SIWA_KEY_ID"), + privateKey: secret("SIWA_PRIVATE_KEY"), + teamId: secret("SIWA_TEAM_ID"), + }, + // Login with Amazon + loginWithAmazon: { + clientId: secret("LOGINWITHAMAZON_CLIENT_ID"), + clientSecret: secret("LOGINWITHAMAZON_CLIENT_SECRET"), + }, + // Facebook Authentication + facebook: { + clientId: secret("FACEBOOK_CLIENT_ID"), + clientSecret: secret("FACEBOOK_CLIENT_SECRET"), + }, + // Callback and logout URLs must be inside externalProviders + callbackUrls: [ + "http://localhost:3000/profile", + "https://mywebsite.com/profile", + ], + logoutUrls: ["http://localhost:3000/", "https://mywebsite.com"], + }, + }, + + // User Attributes Configuration - outside loginWith + userAttributes: { + // Standard attributes examples + email: { + mutable: true, + required: true, + }, + phoneNumber: { + mutable: true, + required: false, + }, + givenName: { + mutable: true, + required: true, + }, + familyName: { + mutable: true, + required: false, + }, + birthdate: { + mutable: true, + required: false, + }, + // Username configuration using preferredUsername + preferredUsername: { + mutable: true, + required: false, + }, + // Additional standard attributes + address: { + mutable: true, + required: false, + }, + gender: { + mutable: true, + required: false, + }, + locale: { + mutable: true, + required: false, + }, + profilePicture: { + mutable: true, + required: false, + }, + website: { + mutable: true, + required: false, + }, + // Custom attributes examples - note the 'custom:' prefix and required dataType + "custom:organization": { + dataType: "String", + mutable: true, + minLen: 3, + maxLen: 100, + }, + "custom:employeeId": { + dataType: "Number", + mutable: false, + min: 1000, + max: 9999999, + }, + "custom:isVerified": { + dataType: "Boolean", + mutable: true, + }, + }, + + // Multi-factor Authentication Configuration + multifactor: { + mode: "OPTIONAL", // Can be OPTIONAL or REQUIRED + sms: true, + totp: false, + }, + + // Account Recovery Configuration + accountRecovery: "EMAIL_AND_PHONE_WITHOUT_MFA", + + // Email Sender Configuration + senders: { + email: { + fromEmail: "registrations@example.com", + }, + }, +}); +``` diff --git a/public/images/gen2/q-developer/general.md b/public/images/gen2/q-developer/general.md new file mode 100644 index 00000000000..7dd16a2396a --- /dev/null +++ b/public/images/gen2/q-developer/general.md @@ -0,0 +1,6 @@ +# AMPLIFYRULES + +1. ALWAYS MENTION WHICH FILE PATH THE USER NEED TO PASTE THE CODE IN. +2. MAKE SURE IF IT IS IN TWO DIFFERENT FILES, YOU DIVIDE THE CODE SNIPPET AND MENTION THE FILE PATH +3. IF THE FILE DOESN'T ALREADY EXIST, GIVE THE STEPS TO GENERATE THE FILES. +4. COMMENT ON EVERY PIECE OF CODE THAT CAN HELP IMPROVE CODE QUALITY THAT EXPLAINS WHAT THE LINE DOES diff --git a/public/images/gen2/q-developer/modeling-relationships.md b/public/images/gen2/q-developer/modeling-relationships.md new file mode 100644 index 00000000000..fc67053fb50 --- /dev/null +++ b/public/images/gen2/q-developer/modeling-relationships.md @@ -0,0 +1,262 @@ +# AMPLIFYRULES + +title: Modeling relationships - AWS Amplify Gen 2 Documentation +source: https://docs.amplify.aws/typescript/build-a-backend/data/data-modeling/relationships/ +framework: typescript +lastModified: 2024-10-21T23:11:46.997Z + +--- + +WHEN MODELING APPLICATION DATA, YOU OFTEN NEED TO ESTABLISH RELATIONSHIPS BETWEEN DIFFERENT DATA MODELS. IN AMPLIFY DATA, YOU CAN CREATE ONE-TO-MANY, ONE-TO-ONE, AND MANY-TO-MANY RELATIONSHIPS IN YOUR DATA SCHEMA. ON THE CLIENT-SIDE, AMPLIFY DATA ALLOWS YOU TO LAZY OR EAGER LOAD OF RELATED DATA. + +```typescript +const schema = a + .schema({ + Member: a.model({ + name: a.string().required(), // 1. Create a reference field teamId: a.id(), + // 2. Create a belongsTo relationship with the reference field + team: a.belongsTo("Team", "teamId"), + }), + Team: a.model({ + mantra: a.string().required(), // 3. Create a hasMany relationship with the reference field + // from the `Member`s model. + members: a.hasMany("Member", "teamId"), + }), + }) + .authorization((allow) => allow.publicApiKey()); +``` + +CREATE A "HAS MANY" RELATIONSHIP BETWEEN RECORDS + +```typescript +const { data: team } = await client.models.Team.create({ + mantra: "Go Frontend!", +}); +const { data: member } = await client.models.Member.create({ + name: "Tim", + teamId: team.id, +}); +``` + +UPDATE A "HAS MANY" RELATIONSHIP BETWEEN RECORDS + +```typescript +const { data: newTeam } = await client.models.Team.create({ + mantra: "Go Fullstack", +}); +await client.models.Member.update({ id: "MY_MEMBER_ID", teamId: newTeam.id }); +``` + +DELETE A "HAS MANY" RELATIONSHIP BETWEEN RECORDS +IF YOUR REFERENCE FIELD IS NOT REQUIRED, THEN YOU CAN "DELETE" A ONE-TO-MANY RELATIONSHIP BY SETTING THE RELATIONSHIP VALUE TO NULL. + +```typescript +await client.models.Member.update({ id: "MY_MEMBER_ID", teamId: null }); +``` + +LAZY LOAD A "HAS MANY" RELATIONSHIP + +```typescript +const { data: team } = await client.models.Team.get({ id: "MY_TEAM_ID" }); +const { data: members } = await team.members(); +members.forEach((member) => console.log(member.id)); +``` + +EAGERLY LOAD A "HAS MANY" RELATIONSHIP + +```typescript +const { data: teamWithMembers } = await client.models.Team.get( + { id: "MY_TEAM_ID" }, + { selectionSet: ["id", "members.*"] } +); +teamWithMembers.members.forEach((member) => console.log(member.id)); +``` + +```typescript +const schema = a + .schema({ + Cart: a.model({ + items: a.string().required().array(), + // 1. Create reference field + customerId: a.id(), + // 2. Create relationship field with the reference field + customer: a.belongsTo("Customer", "customerId"), + }), + Customer: a.model({ + name: a.string(), + // 3. Create relationship field with the reference field + // from the Cart model + activeCart: a.hasOne("Cart", "customerId"), + }), + }) + .authorization((allow) => allow.publicApiKey()); +``` + +CREATE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS +TO CREATE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS, FIRST CREATE THE PARENT ITEM AND THEN CREATE THE CHILD ITEM AND ASSIGN THE PARENT. + +```typescript +const { data: customer, errors } = await client.models.Customer.create({ + name: "Rene", +}); + +const { data: cart } = await client.models.Cart.create({ + items: ["Tomato", "Ice", "Mint"], + customerId: customer?.id, +}); +``` + +UPDATE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS +TO UPDATE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS, YOU FIRST RETRIEVE THE CHILD ITEM AND THEN UPDATE THE REFERENCE TO THE PARENT TO ANOTHER PARENT. FOR EXAMPLE, TO REASSIGN A CART TO ANOTHER CUSTOMER: + +```typescript +const { data: newCustomer } = await client.models.Customer.create({ + name: "Ian", +}); +await client.models.Cart.update({ id: cart.id, customerId: newCustomer?.id }); +``` + +DELETE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS +YOU CAN SET THE RELATIONSHIP FIELD TO NULL TO DELETE A "HAS ONE" RELATIONSHIP BETWEEN RECORDS. + +```typescript +await client.models.Cart.update({ id: project.id, customerId: null }); +``` + +LAZY LOAD A "HAS ONE" RELATIONSHIP + +```typescript +const { data: cart } = await client.models.Cart.get({ id: "MY_CART_ID" }); +const { data: customer } = await cart.customer(); +``` + +EAGERLY LOAD A "HAS ONE" RELATIONSHIP + +```typescript +const { data: cart } = await client.models.Cart.get( + { id: "MY_CART_ID" }, + { selectionSet: ["id", "customer.*"] } +); +console.log(cart.customer.id); +``` + +MODEL A "MANY-TO-MANY" RELATIONSHIP +IN ORDER TO CREATE A MANY-TO-MANY RELATIONSHIP BETWEEN TWO MODELS, YOU HAVE TO CREATE A MODEL THAT SERVES AS A "JOIN TABLE". THIS "JOIN TABLE" SHOULD CONTAIN TWO ONE-TO-MANY RELATIONSHIPS BETWEEN THE TWO RELATED ENTITIES. FOR EXAMPLE, TO MODEL A POST THAT HAS MANY TAGS AND A TAG HAS MANY POSTS, YOU'LL NEED TO CREATE A NEW POSTTAG MODEL THAT RETYPESCRIPTSENTS THE RELATIONSHIP BETWEEN THESE TWO ENTITIES. + +```typescript +const schema = a + .schema({ + PostTag: a.model({ + // 1. Create reference fields to both ends of + // the many-to-many relationshipCopy highlighted code example + postId: a.id().required(), + tagId: a.id().required(), + // 2. Create relationship fields to both ends of + // the many-to-many relationship using their + // respective reference fieldsCopy highlighted code example + post: a.belongsTo("Post", "postId"), + tag: a.belongsTo("Tag", "tagId"), + }), + Post: a.model({ + title: a.string(), + content: a.string(), + // 3. Add relationship field to the join model + // with the reference of `postId`Copy highlighted code example + tags: a.hasMany("PostTag", "postId"), + }), + Tag: a.model({ + name: a.string(), + // 4. Add relationship field to the join model + // with the reference of `tagId`Copy highlighted code example + posts: a.hasMany("PostTag", "tagId"), + }), + }) + .authorization((allow) => allow.publicApiKey()); +``` + +MODEL MULTIPLE RELATIONSHIPS BETWEEN TWO MODELS +RELATIONSHIPS ARE DEFINED UNIQUELY BY THEIR REFERENCE FIELDS. FOR EXAMPLE, A POST CAN HAVE SEPARATE RELATIONSHIPS WITH A PERSON MODEL FOR AUTHOR AND EDITOR. + +```typescript +const schema = a + .schema({ + Post: a.model({ + title: a.string().required(), + content: a.string().required(), + authorId: a.id(), + author: a.belongsTo("Person", "authorId"), + editorId: a.id(), + editor: a.belongsTo("Person", "editorId"), + }), + Person: a.model({ + name: a.string(), + editedPosts: a.hasMany("Post", "editorId"), + authoredPosts: a.hasMany("Post", "authorId"), + }), + }) + .authorization((allow) => allow.publicApiKey()); +``` + +ON THE CLIENT-SIDE, YOU CAN FETCH THE RELATED DATA WITH THE FOLLOWING CODE: + +```typescript +const client = generateClient(); +const { data: post } = await client.models.Post.get({ id: "SOME_POST_ID" }); +const { data: author } = await post?.author(); +const { data: editor } = await post?.editor(); +``` + +MODEL RELATIONSHIPS FOR MODELS WITH SORT KEYS IN THEIR IDENTIFIER +IN CASES WHERE YOUR DATA MODEL USES SORT KEYS IN THE IDENTIFIER, YOU NEED TO ALSO ADD REFERENCE FIELDS AND STORE THE SORT KEY FIELDS IN THE RELATED DATA MODEL: + +```typescript +const schema = a + .schema({ + Post: a.model({ + title: a.string().required(), + content: a.string().required(), + // Reference fields must correspond to identifier fields. + authorName: a.string(), + authorDoB: a.date(), + // Must pass references in the same order as identifiers. + author: a.belongsTo("Person", ["authorName", "authorDoB"]), + }), + Person: a + .model({ + name: a.string().required(), + dateOfBirth: a.date().required(), + // Must reference all reference fields corresponding to the + // identifier of this model. + authoredPosts: a.hasMany("Post", ["authorName", "authorDoB"]), + }) + .identifier(["name", "dateOfBirth"]), + }) + .authorization((allow) => allow.publicApiKey()); +``` + +MAKE RELATIONSHIPS REQUIRED OR OPTIONAL +AMPLIFY DATA'S RELATIONSHIPS USE REFERENCE FIELDS TO DETERMINE IF A RELATIONSHIP IS REQUIRED OR NOT. IF YOU MARK A REFERENCE FIELD AS REQUIRED, THEN YOU CAN'T "DELETE" A RELATIONSHIP BETWEEN TWO MODELS. YOU'D HAVE TO DELETE THE RELATED RECORD AS A WHOLE. + +```typescript +const schema = a + .schema({ + Post: a.model({ + title: a.string().required(), + content: a.string().required(), + // You must supply an author when creating the post + // Author can't be set to `null`. + authorId: a.id().required(), + author: a.belongsTo("Person", "authorId"), + // You can optionally supply an editor when creating the post. + // Editor can also be set to `null`. + editorId: a.id(), + editor: a.belongsTo("Person", "editorId"), + }), + Person: a.model({ + name: a.string(), + editedPosts: a.hasMany("Post", "editorId"), + authoredPosts: a.hasMany("Post", "authorId"), + }), + }) + .authorization((allow) => allow.publicApiKey()); +``` diff --git a/public/images/gen2/q-developer/modeling-schema.md b/public/images/gen2/q-developer/modeling-schema.md new file mode 100644 index 00000000000..009dbfd78e9 --- /dev/null +++ b/public/images/gen2/q-developer/modeling-schema.md @@ -0,0 +1,185 @@ +# AMPLIFYRULES + +# DATA + +- THIS FILE IS TO HELP UNDERSTAND THE RELATIONSHIPS, HOW TO MODEL SCHEMAS, WHAT IS THE CORRECT WAY TO CODE FOR ACCURACY +- USE THIS TO UNDERSTAND HOW DATA SCHEMAS ARE DESIGNED. +- FOR THE DATA SCHEMAS MAKE SURE THAT YOU ALWAYS FOLLOW THESE RULES AND THIS FILE OVER ANY OTHER FILE - THIS IS THE SOURCE OF TRUTH. +- RULES + + - THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR SCHEMA DESIGN AND RELATIONSHIPS. FOLLOW THESE RULES STRICTLY. USE THIS FILE OVER ANY OTHER RESOURCE TO UNDERSTAND SCHEMA DESIGN. + + 1. DON'T USE `.PUBLIC()` WHILE SETTING UP THE AUTHORIZATION. AS AMPLIFY GEN2 ONLY SUPPORTS `.GUEST()`. + 2. `.BEONGSTO()` AND `.HASMANY()` RELATIONS SHALL ALWAYS HAVE THE RELATEDFIELD ID. + 3. `.ENUM()` DOESN'T SUPPORT `.REQUIRED()`/ `.DEFAULTVALUE()` IN ANY CONDITION, SO ALWAYS IGNORE USING IT. + 4. TO GIVE PERMISSION TO THE GROUP MAKE SURE YOU USE .to(), FOLLOWED BY THE GROUP: FOR E.G. `allow.guest().to['read', 'create', 'delete','get'] + 5. THIS IS HOW YOU SHOULD USE THE AUTHORIZATION `(allow) => [allow.owner(),allow.guest().to[("read", "write", "delete")]` , THIS IS INCORRECT `.authorization([allow => allow.owner(), allow => allow.guest().to(['read','write'])])` + +- BELOW ARE THE EXAMPLES TO USE TO GENERATE ANSWERS. + +```typescript +import { type ClientSchema, a, defineData } from "@aws-amplify/backend"; + +const schema = a + .schema({ + Vehicle: a.model({ + id: a.id(), + make: a.string().required(), + model: a.string().required(), + year: a.integer().required(), + licensePlate: a.string().required(), + status: a.enum(["AVAILABLE", "RENTED", "MAINTENANCE"]), // Enum; Don't use .required() or .defaultValue() + locationId: a.id(), + location: a.belongsTo("Location", "locationId"), // Belongs-to relationship, Requires ID + rentals: a.hasMany("Rental", "vehicleId"), // Has-many relationship with required relatedFieldId + }), + Customer: a.model({ + id: a.id(), + firstName: a.string().required(), + lastName: a.string().required(), + email: a.string().required(), + phone: a.string().required(), + licenseNumber: a.string().required(), + rentals: a.hasMany("Rental", "customerId"), // Has-many relationship with required relatedFieldId + }), + Location: a.model({ + id: a.id(), + name: a.string().required(), + address: a.string().required(), + city: a.string().required(), + state: a.string().required(), + zipCode: a.string().required(), + vehicles: a.hasMany("Vehicle", "locationId"), // Has-many relationship with required relatedFieldId + }), + Rental: a.model({ + id: a.id(), + startDate: a.datetime().required(), + endDate: a.datetime().required(), + status: a.enum(["ACTIVE", "COMPLETED", "CANCELLED"]), // Enum; no .required() or .defaultValue() + vehicleId: a.id(), + customerId: a.id(), + vehicle: a.belongsTo("Vehicle", "vehicleId"), // Belongs-to relationship, Requires ID + customer: a.belongsTo("Customer", "customerId"), // Has-many relationship with required relatedFieldId + }), + }) + .authorization((allow) => [ + allow.owner(), + allow.guest().to[("read", "write", "delete")], + ]); // Owner-based and guest access, `.public()` references are replaced with `.guest()`. Authorizaiton groups can be concatenated, To give the permission use the to() function + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: "apiKey", + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); +``` + +- Another Example + +```typescript +import { type ClientSchema, a, defineData } from "@aws-amplify/backend"; + +// Define the schema for the ecommerce application +const schema = a.schema({ + Product: a + .model({ + name: a.string().required(), + description: a.string(), + price: a.float().required(), + inventory: a.integer(), + categoryId: a.id(), + category: a.belongsTo("Category", "categoryId"), // belongs to relationship with required relatedFieldId + images: a.string().array(), + }) + .authorization((allow) => [allow.guest()]), + + Category: a + .model({ + name: a.string().required(), + description: a.string(), + products: a.hasMany("Product", "categoryId"), // Has-many relationship with required relatedFieldId + }) + .authorization((allow) => [allow.guest()]), + + Order: a + .model({ + userId: a.id().required(), + status: a.enum(["PENDING", "PROCESSING", "SHIPPED", "DELIVERED"]), // Enum; Don't use .required() or .defaultValue() + total: a.float().required(), + items: a.hasMany("OrderItem", "orderId"), // Has-many relationship with required relatedFieldId + }) + .authorization((allow) => [allow.owner()]), + + OrderItem: a + .model({ + orderId: a.id().required(), + productId: a.id().required(), + quantity: a.integer().required(), + price: a.float().required(), + }) + .authorization((allow) => [allow.owner()]), +}); + +// Define the client schema and data export +export type Schema = ClientSchema; +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: "userPool", + }, +}); +``` + +```typescript +import { type ClientSchema, a, defineData } from "@aws-amplify/backend"; + +const schema = a + .schema({ + Customer: a + .model({ + customerId: a.id().required(), + // fields can be of various scalar types, + // such as string, boolean, float, integers etc. + name: a.string(), + // fields can be of custom types + location: a.customType({ + // fields can be required or optional + lat: a.float().required(), + long: a.float().required(), + }), + // fields can be enums + engagementStage: a.enum(["PROSPECT", "INTERESTED", "PURCHASED"]), //enum doesn't support required + collectionId: a.id(), + collection: a.belongsTo("Collection", "collectionId"), + // Use custom identifiers. By default, it uses an `id: a.id()` field + }) + .identifier(["customerId"]), + Collection: a + .model({ + customers: a.hasMany("Customer", "collectionId"), // setup relationships between types + tags: a.string().array(), // fields can be arrays + representativeId: a.id().required(), + // customize secondary indexes to optimize your query performance + }) + .secondaryIndexes((index) => [index("representativeId")]), + }) + .authorization((allow) => [allow.publicApiKey()]); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: "apiKey", + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); +``` diff --git a/public/images/user.jpg b/public/images/user.jpg new file mode 100644 index 00000000000..a4e7612efcb Binary files /dev/null and b/public/images/user.jpg differ diff --git a/src/components/AI/index.tsx b/src/components/AI/index.tsx new file mode 100644 index 00000000000..324a0c1a64b --- /dev/null +++ b/src/components/AI/index.tsx @@ -0,0 +1,64 @@ +import { Avatar } from '@aws-amplify/ui-react'; +import { ConversationMessage } from '@aws-amplify/ui-react-ai'; +import { AmplifyLogo } from '@/components/GlobalNav/components/icons'; + +export const UserAvatar = () => { + return ; +}; + +export const AssistantAvatar = () => { + return ( + + + + ); +}; + +export const MESSAGES: ConversationMessage[] = [ + { + conversationId: 'foobar', + id: '1', + content: [{ text: 'Hello' }], + role: 'user' as const, + createdAt: new Date(2023, 4, 21, 15, 23).toISOString() + }, + { + conversationId: 'foobar', + id: '2', + content: [ + { + text: 'Hello! I am your virtual assistant how may I help you?' + } + ], + role: 'assistant' as const, + createdAt: new Date(2023, 4, 21, 15, 24).toISOString() + } +]; + +export const MESSAGES_RESPONSE_COMPONENTS: ConversationMessage[] = [ + { + conversationId: 'foobar', + id: '1', + content: [{ text: 'Whats the weather in San Jose?' }], + role: 'user' as const, + createdAt: new Date(2023, 4, 21, 15, 23).toISOString() + }, + { + conversationId: 'foobar', + id: '2', + content: [ + { + text: 'Let me get the weather for San Jose for you.' + }, + { + toolUse: { + name: 'AMPLIFY_UI_WeatherCard', + input: { city: 'San Jose' }, + toolUseId: '1234' + } + } + ], + role: 'assistant' as const, + createdAt: new Date(2023, 4, 21, 15, 24).toISOString() + } +]; diff --git a/src/components/AIBanner/AIBanner.tsx b/src/components/AIBanner/AIBanner.tsx new file mode 100644 index 00000000000..fd4db18e93a --- /dev/null +++ b/src/components/AIBanner/AIBanner.tsx @@ -0,0 +1,43 @@ +import { Flex, Message, IconsProvider, Text } from '@aws-amplify/ui-react'; +import { IconStar, IconChevron } from '../Icons'; +import { Button } from '@aws-amplify/ui-react'; + +export const AIBanner: React.FC = () => { + const URL = '/react/ai/set-up-ai/'; + return ( + + } + }} + > + + + + + Amplify AI kit is now generally available + + + Create fullstack AI-powered apps with TypeScript, no prior + experience in cloud architecture or AI needed. + + + + + + + + ); +}; diff --git a/src/components/AIBanner/__tests__/AIBanner.test.tsx b/src/components/AIBanner/__tests__/AIBanner.test.tsx new file mode 100644 index 00000000000..60110d8ed8d --- /dev/null +++ b/src/components/AIBanner/__tests__/AIBanner.test.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { AIBanner } from '../index'; + +describe('AIBanner', () => { + it('should render the AIBanner component', async () => { + const bannerText = 'Amplify AI kit is now generally available'; + render(); + + const component = await screen.findByText(bannerText); + expect(component).toBeInTheDocument(); + }); +}); diff --git a/src/components/AIBanner/index.tsx b/src/components/AIBanner/index.tsx new file mode 100644 index 00000000000..8cf7601c7bb --- /dev/null +++ b/src/components/AIBanner/index.tsx @@ -0,0 +1 @@ +export { AIBanner } from './AIBanner'; diff --git a/src/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap b/src/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap index bde562993a2..cd74396813c 100644 --- a/src/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +++ b/src/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Footer should render component that matches snapshot 1`] = `"
"`; +exports[`Footer should render component that matches snapshot 1`] = `"
"`; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 4638d637994..b3419509203 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -29,6 +29,7 @@ import type { HeadingInterface } from '@/components/TableOfContents/TableOfConte import { Breadcrumbs } from '@/components/Breadcrumbs'; import { debounce } from '@/utils/debounce'; import '@docsearch/css'; +import { AIBanner } from '@/components/AIBanner'; import { usePathWithoutHash } from '@/utils/usePathWithoutHash'; import { NextPrevious, @@ -71,6 +72,7 @@ export const Layout = ({ const basePath = 'docs.amplify.aws'; const metaUrl = url ? url : basePath + asPathWithNoHash; const pathname = router.pathname; + const shouldShowAIBanner = asPathWithNoHash === '/'; const isGen1 = asPathWithNoHash.split('/')[1] === 'gen1'; const isContributor = asPathWithNoHash.split('/')[1] === 'contribute'; const currentGlobalNavMenuItem = isContributor ? 'Contribute' : 'Docs'; @@ -272,6 +274,7 @@ export const Layout = ({ platform={currentPlatform} /> ) : null} + {shouldShowAIBanner ? : null} {useCustomTitle ? null : ( {pageTitle} )} diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx index e49cbe2510f..6478c5de608 100644 --- a/src/components/Menu/MenuItem.tsx +++ b/src/components/Menu/MenuItem.tsx @@ -1,6 +1,6 @@ import { usePathWithoutHash } from '@/utils/usePathWithoutHash'; import { ReactElement, useContext, useEffect, useState, useMemo } from 'react'; -import { Link as AmplifyUILink, Flex } from '@aws-amplify/ui-react'; +import { Link as AmplifyUILink, Flex, Badge } from '@aws-amplify/ui-react'; import { IconExternalLink, IconChevron } from '@/components/Icons'; import Link from 'next/link'; import { JS_PLATFORMS, Platform, JSPlatform } from '@/data/platforms'; @@ -200,6 +200,7 @@ export function MenuItem({ className={`menu__list-item__link__inner ${listItemLinkInnerStyle}`} > {pageNode.title} + {pageNode.isNew && New} {children && hasVisibleChildren && level !== Levels.Category && ( )} diff --git a/src/components/UIWrapper/UWrapper.tsx b/src/components/UIWrapper/UWrapper.tsx new file mode 100644 index 00000000000..e0ca6f94bd8 --- /dev/null +++ b/src/components/UIWrapper/UWrapper.tsx @@ -0,0 +1,29 @@ +import { + createTheme, + defaultDarkModeOverride, + ThemeProvider, + View +} from '@aws-amplify/ui-react'; +import * as React from 'react'; +import { LayoutContext } from '../Layout'; + +const theme = createTheme({ + name: 'default-amplify-ui-theme', + overrides: [defaultDarkModeOverride] +}); + +export const UIWrapper = ({ children }: React.PropsWithChildren) => { + const { colorMode } = React.useContext(LayoutContext); + + return ( + + + {children} + + + ); +}; diff --git a/src/components/UIWrapper/index.ts b/src/components/UIWrapper/index.ts new file mode 100644 index 00000000000..984a3cb3738 --- /dev/null +++ b/src/components/UIWrapper/index.ts @@ -0,0 +1 @@ +export { UIWrapper } from './UWrapper'; diff --git a/src/directory/directory.d.ts b/src/directory/directory.d.ts index 5d1433a162b..ad0fd85e291 100644 --- a/src/directory/directory.d.ts +++ b/src/directory/directory.d.ts @@ -57,4 +57,9 @@ export type PageNode = { * This is being used for categories like Cli - Legacy and SDK */ hideChildrenOnBase?: boolean; + + /** + * This flag indicates that the item is new and will display a "new" badge + */ + isNew?: boolean; }; diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index d6337968899..4437768235f 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -73,6 +73,9 @@ export const directory = { { path: 'src/pages/[platform]/build-a-backend/auth/concepts/phone/index.mdx' }, + { + path: 'src/pages/[platform]/build-a-backend/auth/concepts/passwordless/index.mdx' + }, { path: 'src/pages/[platform]/build-a-backend/auth/concepts/user-attributes/index.mdx' }, @@ -137,6 +140,9 @@ export const directory = { { path: 'src/pages/[platform]/build-a-backend/auth/manage-users/manage-passwords/index.mdx' }, + { + path: 'src/pages/[platform]/build-a-backend/auth/manage-users/manage-webauthn-credentials/index.mdx' + }, { path: 'src/pages/[platform]/build-a-backend/auth/manage-users/manage-devices/index.mdx' }, @@ -286,6 +292,9 @@ export const directory = { }, { path: 'src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-http-datasource/index.mdx' + }, + { + path: 'src/pages/[platform]/build-a-backend/data/custom-business-logic/batch-ddb-operations/index.mdx' } ] }, @@ -331,6 +340,9 @@ export const directory = { }, { path: 'src/pages/[platform]/build-a-backend/data/aws-appsync-apollo-extensions/index.mdx' + }, + { + path: 'src/pages/[platform]/build-a-backend/data/enable-logging/index.mdx' } ] }, @@ -442,6 +454,9 @@ export const directory = { }, { path: 'src/pages/[platform]/build-a-backend/functions/modify-resources-with-cdk/index.mdx' + }, + { + path: 'src/pages/[platform]/build-a-backend/functions/custom-functions/index.mdx' } ] }, @@ -716,6 +731,9 @@ export const directory = { }, { path: 'src/pages/[platform]/build-a-backend/troubleshooting/cannot-find-module-amplify-env/index.mdx' + }, + { + path: 'src/pages/[platform]/build-a-backend/troubleshooting/circular-dependency/index.mdx' } ] } @@ -753,6 +771,12 @@ export const directory = { { path: 'src/pages/[platform]/ai/conversation/index.mdx', children: [ + { + path: 'src/pages/[platform]/ai/conversation/ai-conversation/index.mdx' + }, + { + path: 'src/pages/[platform]/ai/conversation/connect-your-frontend/index.mdx' + }, { path: 'src/pages/[platform]/ai/conversation/history/index.mdx' }, @@ -771,7 +795,12 @@ export const directory = { ] }, { - path: 'src/pages/[platform]/ai/generation/index.mdx' + path: 'src/pages/[platform]/ai/generation/index.mdx', + children: [ + { + path: 'src/pages/[platform]/ai/generation/data-extraction/index.mdx' + } + ] } ] }, diff --git a/src/fragments/lib-v1/geo/js/geofences.mdx b/src/fragments/lib-v1/geo/js/geofences.mdx index f57fccd2e54..89dd2393188 100644 --- a/src/fragments/lib-v1/geo/js/geofences.mdx +++ b/src/fragments/lib-v1/geo/js/geofences.mdx @@ -81,7 +81,7 @@ export default withAuthenticator(Map); -**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. +**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/maplibre/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. ```javascript import React from 'react'; diff --git a/src/fragments/lib-v1/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx b/src/fragments/lib-v1/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx index 7775307ceca..701e089d8c1 100644 --- a/src/fragments/lib-v1/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx +++ b/src/fragments/lib-v1/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx @@ -15,5 +15,5 @@ import reactnative1 from '/src/fragments/lib-v1/in-app-messaging/integrate-your- ```bash -npm install @aws-amplify/ui-react-native react-native-safe-area-context +npm install @aws-amplify/ui-react-native react-native-safe-area-context@^4.2.5 ``` diff --git a/src/fragments/lib-v1/ssr/js/next-js-callout.mdx b/src/fragments/lib-v1/ssr/js/next-js-callout.mdx index 85fe4b6bafd..eed5f6c322a 100644 --- a/src/fragments/lib-v1/ssr/js/next-js-callout.mdx +++ b/src/fragments/lib-v1/ssr/js/next-js-callout.mdx @@ -1,5 +1,5 @@ -SSR functionality in Amplify was primarily built for compatibility with Next.js [page components](https://nextjs.org/docs/basic-features/pages), and their [getServerSideProps data fetching mechanism](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props). Compatibility with other frameworks or Next.js features cannot be guaranteed. +SSR functionality in Amplify was primarily built for compatibility with Next.js [page components](https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts), and their [getServerSideProps data fetching mechanism](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props). Compatibility with other frameworks or Next.js features cannot be guaranteed. - \ No newline at end of file + diff --git a/src/fragments/lib-v1/start/getting-started/next/setup.mdx b/src/fragments/lib-v1/start/getting-started/next/setup.mdx index a1575bb82cb..420757f976c 100644 --- a/src/fragments/lib-v1/start/getting-started/next/setup.mdx +++ b/src/fragments/lib-v1/start/getting-started/next/setup.mdx @@ -17,7 +17,7 @@ To set up the project, you will need to create a new Next.js app with the [`crea Run the following command and following the instructions to create a Next.js app. ```shellscript -npx create-next-app@">=13.5.0 <15.0.0" next-amplified +npx create-next-app@">=13.5.0 <16.0.0" next-amplified ``` Then run the following command to enter the root of your Next.js app. diff --git a/src/fragments/lib/geo/js/geofences.mdx b/src/fragments/lib/geo/js/geofences.mdx index 8b9b36636c0..54966e298a5 100644 --- a/src/fragments/lib/geo/js/geofences.mdx +++ b/src/fragments/lib/geo/js/geofences.mdx @@ -80,7 +80,7 @@ export default withAuthenticator(Map); -**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. +**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/maplibre/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. ```javascript import React from 'react'; diff --git a/src/fragments/lib/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx b/src/fragments/lib/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx index 1aacbf80a69..d4d7acb9add 100644 --- a/src/fragments/lib/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx +++ b/src/fragments/lib/in-app-messaging/integrate-your-application/react-native/install-dependencies.mdx @@ -22,5 +22,5 @@ import reactnative1 from '/src/fragments/lib/in-app-messaging/integrate-your-app ```bash -npm install @aws-amplify/ui-react-native react-native-safe-area-context +npm install @aws-amplify/ui-react-native react-native-safe-area-context@^4.2.5 ``` diff --git a/src/fragments/lib/ssr/nextjs/version-range-callout.mdx b/src/fragments/lib/ssr/nextjs/version-range-callout.mdx index 675824dd3e9..0cca2aa70d7 100644 --- a/src/fragments/lib/ssr/nextjs/version-range-callout.mdx +++ b/src/fragments/lib/ssr/nextjs/version-range-callout.mdx @@ -1,6 +1,6 @@ -**NOTE:** Amplify JS v6 supports Next.js with the version range: `>=13.5.0 <15.0.0`. +**NOTE:** Amplify JS v6 supports Next.js with the version range: `>=13.5.0 <16.0.0`. Ensure you have the correct version to integrate with Amplify. diff --git a/src/fragments/lib/troubleshooting/common/upgrading.mdx b/src/fragments/lib/troubleshooting/common/upgrading.mdx index d488d0b24c1..f92221c2b57 100644 --- a/src/fragments/lib/troubleshooting/common/upgrading.mdx +++ b/src/fragments/lib/troubleshooting/common/upgrading.mdx @@ -136,7 +136,7 @@ npm install aws-amplify@6 @aws-amplify/adapter-nextjs The `@aws-amplify/adapter-nextjs` package provides adapter functions to enable use of Amplify APIs on the server side of your Next.js app for use cases such as Server Side Rendering (SSR) with the App Router. -Note that v6 supports NextJS v13.5.0 through 14. We recommend upgrading if you are using a version below 13.5.0. +Note that v6 supports NextJS v13.5.0 through 15. We recommend upgrading if you are using a version below 13.5.0. NextJS v13.5.0 requires Node v16.14.0 or later and NextJS v14+ requires Node v18.17.0 or later @@ -151,7 +151,7 @@ amplify upgrade amplify push ``` -This will generate a new configuration file called `amplifyconfiguration.json` +This will generate a new configuration file called `amplifyconfiguration.json` (You may need to use `amplify pull` if there are no changes in your environment). Wherever you called `Amplify.configure({ aws-exports });` previously (usually in the root of your project) update your code as shown below diff --git a/src/fragments/start/getting-started/next/setup.mdx b/src/fragments/start/getting-started/next/setup.mdx index 47329c57e1b..0482b108c38 100644 --- a/src/fragments/start/getting-started/next/setup.mdx +++ b/src/fragments/start/getting-started/next/setup.mdx @@ -1,7 +1,6 @@ This tutorial is built using the pages directory from NextJS. To learn more about using Amplify with the NextJS app directory please visit this [documentation page](/gen1/[platform]/build-a-backend/server-side-rendering/set-up-ssr/#use-amplify-with-nextjs-app-router-app-directory). -**Note:** We currently support Next.js versions 13.5.0 up to 14.x. We are working to support version 15 or newer. @@ -10,7 +9,7 @@ To set up the project, you'll first create a new Next.js app with [Create Next A From your projects directory, run the following commands: ```bash -npx create-next-app@">=13.5.0 <15.0.0" next-amplified --no-app +npx create-next-app@">=13.5.0 <16.0.0" next-amplified --no-app cd next-amplified ``` diff --git a/src/fragments/start/getting-started/reactnative/auth.mdx b/src/fragments/start/getting-started/reactnative/auth.mdx index 123224de944..7775a1785fd 100644 --- a/src/fragments/start/getting-started/reactnative/auth.mdx +++ b/src/fragments/start/getting-started/reactnative/auth.mdx @@ -45,14 +45,14 @@ The `@aws-amplify/ui-react-native` package includes React Native specific UI com ```bash -npm install @aws-amplify/ui-react-native react-native-get-random-values react-native-url-polyfill +npm install @aws-amplify/ui-react-native react-native-safe-area-context@^4.2.5 react-native-get-random-values react-native-url-polyfill ``` ```bash -npm install @aws-amplify/ui-react-native react-native-safe-area-context react-native-get-random-values react-native-url-polyfill +npm install @aws-amplify/ui-react-native react-native-safe-area-context@^4.2.5 react-native-get-random-values react-native-url-polyfill ``` You will also need to install the pod dependencies for iOS: diff --git a/src/pages/[platform]/ai/concepts/inference-configuration/index.mdx b/src/pages/[platform]/ai/concepts/inference-configuration/index.mdx index 303203fc3e1..14aa326ad1e 100644 --- a/src/pages/[platform]/ai/concepts/inference-configuration/index.mdx +++ b/src/pages/[platform]/ai/concepts/inference-configuration/index.mdx @@ -50,7 +50,7 @@ All generative AI routes in Amplify accept inference configuration as optional p ```ts a.generation({ - aiModel: a.ai.model("Claude 3 Haiku"), + aiModel: a.ai.model("Claude 3.5 Haiku"), systemPrompt: `You are a helpful assistant`, inferenceConfiguration: { temperature: 0.2, diff --git a/src/pages/[platform]/ai/concepts/models/index.mdx b/src/pages/[platform]/ai/concepts/models/index.mdx index fd531c7d4aa..7dc316b4ae6 100644 --- a/src/pages/[platform]/ai/concepts/models/index.mdx +++ b/src/pages/[platform]/ai/concepts/models/index.mdx @@ -1,4 +1,5 @@ import { getCustomStaticPath } from "@/utils/getCustomStaticPath"; +import { Table, TableBody, TableCell, TableHead, TableRow } from '@aws-amplify/ui-react'; export const meta = { title: "Models", @@ -30,12 +31,10 @@ export function getStaticProps(context) { A foundation model is a large, general-purpose machine learning model that has been pre-trained on a vast amount of data. These models are trained in an unsupervised or self-supervised manner, meaning they learn patterns and representations from the unlabeled training data without being given specific instructions or labels. -Foundation models are useful because they are general-purpose and you don't need to train the models yourself, but are powerful enough to take on a range of applications. +Foundation models are useful because they are general-purpose and you don't need to train the models yourself, but are powerful enough to take on a range of applications. Foundation Models, which Large Language Models are a part of, are inherently stateless. They take input in the form of text or images and generate text or images. They are also inherently non-deterministic. Providing the same input can generate different output. - - ## Getting model access Before you can invoke a foundation model on Bedrock you will need to [request access to the models in the AWS console](https://console.aws.amazon.com/bedrock/home#/modelaccess). @@ -44,46 +43,137 @@ Be sure to check the region you are building your Amplify app in! ## Pricing and Limits -Each foundation model in Amazon Bedrock has its own pricing and throughput limits for on-demand use. On-demand use is serverless, you don't need to provision any AWS resources to use and you only pay for what you use. The Amplify AI kit uses on-demand use for Bedrock. +Each foundation model in Amazon Bedrock has its own pricing and throughput limits for on-demand use. On-demand use is serverless, you don't need to provision any AWS resources to use and you only pay for what you use. The Amplify AI kit uses on-demand use for Bedrock. -The cost for using foundation models is calculated by token usage. A token in generative AI refers to chunks of data that were sent as input and how much data was generated. A token is roughly equal to a word, but depends on the model being used. Each foundation model in Bedrock has its own pricing based on input and output tokens used. +The cost for using foundation models is calculated by token usage. A token in generative AI refers to chunks of data that were sent as input and how much data was generated. A token is roughly equal to a word, but depends on the model being used. Each foundation model in Bedrock has its own pricing based on input and output tokens used. When you use the Amplify AI Kit, inference requests are charged to your AWS account based on Bedrock pricing. There is no Amplify markup, you are just using AWS resources in your own account. Always refer to [Bedrock pricing](https://aws.amazon.com/bedrock/pricing/) for the most up-to-date information on running generative AI with Amplify AI Kit. + + Your Amplify project must be deployed to a region where the foundation model you specify is available. See [Bedrock model support](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) for the supported regions per model. + ## Supported Providers and Models -The Amplify AI Kit uses Bedrock's [Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html) to leverage a unified API across models. Most models have different structures to how they best work with input and how they format their output. For example, ... - -### AI21 Labs -* [Jamba 1.5 Large](https://aws.amazon.com/blogs/aws/jamba-1-5-family-of-models-by-ai21-labs-is-now-available-in-amazon-bedrock/) -* [Jamba 1.5 Mini](https://aws.amazon.com/blogs/aws/jamba-1-5-family-of-models-by-ai21-labs-is-now-available-in-amazon-bedrock/) - - -### Anthropic -* Claude 3 Haiku -* Claude 3 Sonnet -* Claude 3 Opus -* Claude 3.5 Sonnet -https://docs.anthropic.com/en/docs/about-claude/models - -### Cohere -* Command R -* Command R+ - -### Meta -* Llama 3.1 - -### Mistral -* Large -* Large 2 - - -The Amplify AI Kit makes use of ["tools"](/[platform]/ai/concepts/tools) for both generation and conversation routes. [The models it supports must support tool use in the Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html). - -Using the Converse API makes it easy to swap different models without having to drastically change how you interact with them. +The Amplify AI Kit uses Bedrock's [Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html) to leverage a unified API across models. + + + + + Provider + Model + Conversation + Generation + + + + + AI21 Labs + Jurassic-2 Large + + + + + AI21 Labs + Jurassic-2 Mini + + + + + Amazon + Amazon Nova Pro + + + + + Amazon + Amazon Nova Lite + + + + + Amazon + Amazon Nova Micro + + + + + Anthropic + Claude 3 Haiku + + + + + Anthropic + Claude 3.5 Haiku + + + + + Anthropic + Claude 3 Sonnet + + + + + Anthropic + Claude 3.5 Sonnet + + + + + Anthropic + Claude 3.5 Sonnet v2 + + + + + Anthropic + Claude 3 Opus + + + + + Cohere + Command R + + + + + Cohere + Command R+ + + + + + Meta + Llama 3.1 + + + + + Mistral AI + Large + + + + + Mistral AI + Large 2 + + + + +
+ +Amplify AI Kit makes use of ["tools"](/[platform]/ai/concepts/tools) for both generation and conversation routes. [The models used must support tool use in the Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html). + +Most models have different structures to how they best work with input and how they format their output. Using the Converse API makes it easy to swap different models without having to drastically change how you interact with them. ## Choosing a model @@ -95,17 +185,17 @@ Each model has its own context window size. The context window is how much infor ### Latency -Smaller models tend to have a lower latency than larger models, but can also sometimes be less powerful. +Smaller models tend to have a lower latency than larger models, but can also sometimes be less powerful. ### Cost -Each model has its own price and throughput. +Each model has its own price and throughput. ### Use-case fit -Some models are trained to be better at certain tasks or with certain languages. +Some models are trained to be better at certain tasks or with certain languages. -Choosing the right model for your use case is balancing latency, cost, and performance. +Choosing the right model for your use case is balancing latency, cost, and performance. ## Using different models @@ -115,7 +205,7 @@ Using the Amplify AI Kit you can easily use different models for different funct ```ts const schema = a.schema({ summarizer: a.generation({ - aiModel: a.ai.model("Claude 3 Haiku") + aiModel: a.ai.model("Claude 3.5 Haiku") }) }) ``` @@ -131,5 +221,3 @@ const schema = a.schema({ }) }) ``` - - diff --git a/src/pages/[platform]/ai/concepts/prompting/index.mdx b/src/pages/[platform]/ai/concepts/prompting/index.mdx index 0a5ce6e67b8..2466b3a7897 100644 --- a/src/pages/[platform]/ai/concepts/prompting/index.mdx +++ b/src/pages/[platform]/ai/concepts/prompting/index.mdx @@ -52,7 +52,7 @@ All AI routes in the Amplify AI kit require a system prompt. This will be used i ```ts reviewSummarizer: a.generation({ - aiModel: a.ai.model("Claude 3.5 Sonnet"), + aiModel: a.ai.model("Claude 3.5 Haiku"), systemPrompt: ` You are a helpful assistant that summarizes reviews for an ecommerce site. diff --git a/src/pages/[platform]/ai/conversation/ai-conversation/index.mdx b/src/pages/[platform]/ai/conversation/ai-conversation/index.mdx new file mode 100644 index 00000000000..cc5b19687ea --- /dev/null +++ b/src/pages/[platform]/ai/conversation/ai-conversation/index.mdx @@ -0,0 +1,371 @@ +import { Card, Text } from '@aws-amplify/ui-react'; +import { AIConversation } from '@aws-amplify/ui-react-ai' +import { getCustomStaticPath } from "@/utils/getCustomStaticPath"; +import { UIWrapper } from '@/components/UIWrapper' +import { UserAvatar,AssistantAvatar, MESSAGES, MESSAGES_RESPONSE_COMPONENTS } from '@/components/AI' + +export const meta = { + title: "", + description: + "The AIConversation component is a customizable chat interface built for the Amplify AI kit", + platforms: [ + "nextjs", + "react", + ], +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta, + }, + }; +} + + + {}} +/> + +*Note: the example is a mocked component and not hooked up to a live backend* + +## Introduction + +The `` component is highly customizable to fit into any application. The component is built so that it works with the `useAIConversation` hook. The hook manages the state and lifecycle of the component. The component by itself is just a renderer for the conversation state, which the hook provides. The `` component requires some props: +* `messages` an array of the messages in the conversation +* `handleSendMessage` a handler that is called when a user message is sent. + +The `useAIConversation` hook provides these values and manages the messages state as user messages are sent and assistant responses are streamed back. + +```tsx title="Mock conversation" +import { AIConversation } from '@aws-amplify/ui-react-ai'; + +export default function Chat() { + return ( + {}} + /> + ) +} +``` + +The code above won't really do much, but if you wanted to play around with the component or visually test how it will look, you can do that passing in your own set of messages. + + +## Getting started + +Make sure to first follow our [getting started guide for the Amplify AI kit](/[platform]/ai/set-up-ai) to set up your Amplify AI backend. + +Conversations required a logged in user, so we recommend using the [``](/[platform]/build-a-backend/auth/connect-your-frontend/using-the-authenticator/) component to easily add authentication flows to your app. + + + +```tsx title="src/App.tsx" +import { Amplify } from 'aws-amplify'; +import { generateClient } from "aws-amplify/api"; +import { Authenticator } from "@aws-amplify/ui-react"; +import { AIConversation, createAIHooks } from '@aws-amplify/ui-react-ai'; +import '@aws-amplify/ui-react/styles.css'; +import outputs from "../amplify_outputs.json"; +import { Schema } from "../amplify/data/resource"; + +Amplify.configure(outputs); + +const client = generateClient({ authMode: "userPool" }); +const { useAIConversation } = createAIHooks(client); + +export default function App() { + const [ + { + data: { messages }, + isLoading, + }, + handleSendMessage, + ] = useAIConversation('chat'); + // 'chat' is based on the key for the conversation route in your schema. + + return ( + + + + ); +} +``` + + + +## Formatting Markdown + +LLMs can respond with markdown. The `` component does not have built-in markdown rendering, but does allow for you to pass in your own markdown renderer. + +```tsx +import ReactMarkdown from 'react-markdown'; + + {text} + }} +/> +``` + +The `messageRenderer` property lets you customize how markdown is rendered within the chat according to your application's needs. The example below demonstrates how to add code syntax highlighting by using `ReactMarkdown` with `rehypeHighlight`. + +```tsx +import ReactMarkdown from 'react-markdown'; +import rehypeHighlight from 'rehype-highlight'; + + ( + + {text} + + ) + }} +/> +``` + +## Rendering images + +The `` component renders images in the conversation history by default. You can also customize how images are rendered with `messageRenderer`, similar to the text example above. + +```tsx +// Note: the image in a message comes in as a byte array +// you will need to convert this to base64 +function convertBufferToBase64( + buffer: ArrayBuffer, + format: 'png' | 'jpeg' | 'gif' | 'webp' +): string { + const base64string = Buffer.from(new Uint8Array(buffer)).toString('base64'); + return `data:image/${format};base64,${base64string}`; +} + + ( + + ), + }} +/> +``` + +## Welcome message + +You can have the `` component display a welcome message when a user starts a new conversation. + + + {}} + welcomeMessage={ + + I am your virtual assistant, ask me any questions you like! + + } +/> + + +```tsx + + I am your virtual assistant, ask me any questions you like! + + } +/> +``` + +The welcome message will disappear once a message has been sent. + +## Customizing the timestamp + +All messages have a timestamp associated with them that are displayed next to the username. To customize how the timestamp displays you can pass a custom text formatter function called `getMessageTimestampText` into the `displayText` property on the `` component. This function will receive a `Date` object as its argument and should return a string. + +Browsers have a really nice built-in date/time formatter you can use called [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). + + + {}} + displayText={{ + getMessageTimestampText: (date) => new Intl.DateTimeFormat('en-US', { + timeStyle: 'short', + hour12: true, + timeZone: 'UTC' + }).format(date) + }} +/> + + +```tsx + new Intl.DateTimeFormat('en-US', { + timeStyle: 'short', + hour12: true, + timeZone: 'UTC' + }).format(date) + }} +/> +``` + +You could also return an empty string if you wanted to hide the timestamps altogether. + +## Attachments + +Some of the newer LLMs like the Claude 3 family of models from Anthropic support multi-modal input, so you can send images in your message to the model and it can respond based on the messages. To enable this functionality in the component, there is an `allowAttachments` prop you can enable. + +There are some limitations on the filetype and size of the images attached. The file size for each file should be below 400kb when base64 encoded. Also the currently supported file types are: png, jpg, gif, and webp. + + + + {}} + allowAttachments +/> + + +```tsx + +``` + + +## Avatars + +You can customize the usernames and avatars used in the `AIConversation` component by using the `avatars` prop. This lets you control what your AI assistant looks like in the chat and what your user's username and avatar are. + +There are 2 avatars, `user` and `ai`, and each have a `username` and `avatar` attribute. The `avatar` is a React Node and the `username` is a string. + + + {}} + allowAttachments + avatars={{ + user: { + avatar: , + username: "danny" + }, + ai: { + avatar: , + username: "Amplify assistant" + } + }} +/> + + + +```tsx +, + username: "danny", + }, + ai: { + avatar: , + username: "Amplify assistant" + } + }} +/> +``` + + +## Response components + +Response components are a way to define custom UI components that the LLM can respond with in the conversation. This creates a richer experience than just text responses so the conversation can be more interactive and engaging. To define a response component you need any React component and give it a name, description, and define the props the LLM should know. + + + {}} + responseComponents={{ + WeatherCard: { + description: + 'Used to display the weather of a given city to the user', + component: ({ city }) => { + return ( + + {city} + + ); + }, + props: { + city: { + type: 'string', + required: true, + }, + }, + }, + }} +/> + + + +```tsx + { + return {city}; + }, + props: { + city: { + type: 'string', + required: true, + }, + }, + }, + }} +/> +``` + +Response components are just plain React components; they can have their own interactive state, fetch data, update shared state, or really anything you can think of. You can pair response components with [data tools](/[platform]/ai/conversation/tools), so the LLM can query for some data and then use a component to display that data. Or your response component could fetch data itself. + +### Adding a fallback + +Because response components are defined at runtime and conversation histories are stored in a database, there can be times when there is a response component in the message history that the current application does not have. Response components are saved in the message history as a "toolUse" block, similar to how an LLM would respond when it wants to call a tool. The toolUse block contains the name of the component, and the props the LLM wanted to pass to the component. The LLM is never directly sending UI code, but rather an abstract representation of what it wants to render. + +If the AIConversation component receives a response component message for a response component that was not given to it, by default it will just not render anything. However if you want to add a fallback component if no component is found based on the name, you can use the `FallbackResponseComponent` prop. You can think of this like a 404 page for response components. + + + {}} + FallbackResponseComponent={(props) => {JSON.stringify(props, null, 2)}} +/> + + +```tsx + ( + {JSON.stringify(props, null, 2)} + )} +/> +``` + + + diff --git a/src/pages/[platform]/ai/conversation/connect-your-frontend/index.mdx b/src/pages/[platform]/ai/conversation/connect-your-frontend/index.mdx new file mode 100644 index 00000000000..911f0c77afb --- /dev/null +++ b/src/pages/[platform]/ai/conversation/connect-your-frontend/index.mdx @@ -0,0 +1,449 @@ +import { getCustomStaticPath } from "@/utils/getCustomStaticPath"; + +export const meta = { + title: "Connect your frontend", + description: "Learn how to use AI conversations in your app", + platforms: [ + "javascript", + "react-native", + "angular", + "nextjs", + "react", + "vue", + ], +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta, + showBreadcrumbs: false, + }, + }; +} + +In this guide, you will learn how to create, update, and delete conversations, as well as send messages and subscribe to assistant responses. + +Conversations and their associated messages are persisted in Amazon DynamoDB. This means the previous messages for a conversation are automatically included in the history sent to the LLM. Access to conversations and messages are scoped to individual users through the [owner based authorization strategy](/react/build-a-backend/data/customize-authz/per-user-per-owner-data-access/). + +## Conversation and Message types + +### Conversation + +There are two main types within the conversation flow, `Conversation` and `Message`. + +A `Conversation` is an instance of a chat session between an application user and an LLM. It contains data and methods for interacting with the conversation. A conversation has a one-to-many relationship with its messages. + +The `Conversation` type is accessible via `Schema['myChat']['type']` type definition, where `'myChat'` is the name of the conversation route in your data schema. + +| Property/Method | Type | Description | +|------------------|:-------|:-------------| +| `id` | `string` | The unique identifier for the conversation | +| `name` | `string \| undefined` | The name of the conversation.
You can specify a name when creating or updating a conversation. | +| `metadata` | `Record \| undefined` | Metadata associated with the conversation.
You can specify arbitrary metadata when creating or updating a conversation. | +| `createdAt` | `string` | The date and time when the conversation was created |w +| `updatedAt` | `string` | The date and time when the last user message was sent | +| `sendMessage()` | `(content: MessageContent) => {`
  `data: Message;`
  `errors: Error[]`
`}` | Send a message to the AI assistant | +| `listMessages()` | `() => {`
  `data: Message[];`
  `errors: Error[]`
`}` | List all messages for the conversation | +| `onStreamEvent()`| `(options: {`
  `next: (event: StreamEvent) => void;`
  `error: (error: Error) => void;`
`}) => void` | Subscribe to assistant responses | + +### Message + +A `Message` is a single chat message between an application user and an LLM. Each message has a `role` property that indicates whether the message is from the user or the assistant. User and assistant messages have a one-to-one relationship. Assistant messages contain an `associatedUserMessageId` property that points to the `id` of the user message that triggered the assistant response. + +The `Message` type is accessible via `Schema['myChat']['messageType']`, where `'myChat'` is the name of the conversation route in your data schema. + +| Property | Type | Description | +|-----------|:-------|:-------------| +| `id` | `string` | The unique identifier for the message | +| `conversationId` | `string` | The ID of the conversation this message belongs to | +| `associatedUserMessageId` | `string \| undefined` | For assistant messages, the ID of the user message that triggered the response | +| `content` | `MessageContent[]` | The content of the message | +| `role` | `'user' \| 'assistant'` | Whether the message is from the user or assistant | +| `createdAt` | `string` | The date and time when the message was created | + +## Request response flow + +1. Create a new conversation with `.create()` or get an existing one with `.get()`. +2. Subscribe to assistant responses for a conversation with `.onStreamEvent()`. +3. Send messages to the conversation with `.sendMessage()`. + +```ts +import { generateClient } from 'aws-amplify/data'; +import { type Schema } from '../amplify/data/resource' + +const client = generateClient(); + +// 1. Create a conversation +const { data: chat, errors } = await client.conversations.chat.create(); + +// 2. Subscribe to assistant responses +const subscription = chat.onStreamEvent({ + next: (event) => { + // handle assistant response stream events + console.log(event); + }, + error: (error) => { + // handle errors + console.error(error); + }, +}); + +// 3. Send a message to the conversation +const { data: message, errors } = await chat.sendMessage('Hello, world!'); +``` + +## Managing conversations + +### Create a conversation + +Create a new conversation by calling the `.create()` method on your conversation route. In the examples below, we're using a conversation route named `chat`. + +```ts +const { data: chat, errors } = await client.conversations.chat.create(); + +/** +Example conversation data +{ + id: '123e4567-e89b-12d3-a456-426614174000', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +} +*/ +``` + +You can optionally attach a `name` and `metadata` to a conversation by passing them as arguments to the `.create()` method. There are no uniqueness constraints on conversation `name` or `metadata` values. + +```ts +const { data: chat, errors } = await client.conversations.chat.create({ + name: 'My conversation', + metadata: { + value: '1234567890', + }, +}); + +/** +Example conversation data +{ + id: '123e4567-e89b-12d3-a456-426614174000', + name: 'My conversation', + metadata: { + value: '1234567890', + }, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +} +*/ +``` + +### Get an existing conversation + +You can get an existing conversation by calling the `.get()` method on your conversation route with the conversation's `id`. + +```ts +const id = '123e4567-e89b-12d3-a456-426614174000'; +const { data: chat, errors } = await client.conversations.chat.get({ id }); + +/** +Example conversation data +{ + id: '123e4567-e89b-12d3-a456-426614174000', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +} +*/ +``` + +### List conversations + +You can list all conversations for a user with the `.list()` method. Retrieved conversations are sorted by `updatedAt` in descending order. This means the most recently used conversations are returned first. + +```ts +const { data: chat, errors } = await client.conversations.chat.list(); + +/** +Example conversations data +{ + items: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + ... + ], + nextToken: '...', +} +*/ +``` + +Use the `nextToken` value to paginate through conversations and optionally specify a `limit` to limit the number of conversations returned. + +```ts +const { data: chat, errors } = await client.conversations.chat.list({ + limit: 10, + nextToken: '...', +}); +``` + +### Update a conversation + +You can update a conversation's `name` and `metadata` with the `.update()` method. + +This is useful if you want to update the conversation name based on the messages sent or attach arbitrary metadata at a later time. + +```ts +const id = '123e4567-e89b-12d3-a456-426614174000'; +const { data: chat, errors } = await client.conversations.chat.update({ + id, + name: 'My updated conversation', +}); + +/** +Example conversation data +{ + id: '123e4567-e89b-12d3-a456-426614174000', + name: 'My updated conversation', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-02T00:00:00.000Z', +} +*/ +``` + +### Delete a conversation + + + Deleting a conversation makes it unusable in the future. However it does not delete its associated messages. + + +```ts +const id = '123e4567-e89b-12d3-a456-426614174000'; +const { data: chat, errors } = await client.conversations.chat.delete({ id }); +``` + +## Using a conversation instance + +Once you have a conversation instance, you can interact with it by calling methods on the instance. These methods are documented in the [Conversation and Message types](#conversation-and-message-types) section. + +### Send a message + +Once you have a conversation instance, you can send a message to the AI assistant by calling the `.sendMessage()` method. In its simplest form you just pass the message content as text. + + + The message returned is the user message sent. Assistant messages are streamed back to the client and can be subscribed to with the `.onStreamEvent()` method. See [Subscribe to assistant responses](#subscribe-to-assistant-responses) for more information. + + +```ts +const { data: message, errors } = await chat.sendMessage('Hello, world!'); + +/** +Example message data +{ + id: '98765432-dcba-4321-9876-543210987654', + conversationId: '123e4567-e89b-12d3-a456-426614174000', + content: 'Hello, world!', + role: 'user', + createdAt: '2024-01-01T00:00:00.000Z', +} +*/ +``` + +There are other arguments you can pass to `.sendMessage()` to customize the message according to your application's needs. + +#### Customizing the message content + +`sendMessage()` accepts a object type with a `content` property that provides a flexible way to send different types of content to the AI assistant. + +##### Image Content +Use `image` to send an image to the AI assistant. +Supported image formats are `png`, `gif`, `jpeg`, and `webp`. + +```ts +const { data: message, errors } = await chat.sendMessage({ + content: [ + { + image: { + format: 'png', + source: { + bytes: new Uint8Array([1, 2, 3]), + }, + }, + }, + ], +}); +``` + +Mixing `text` and `image` in a single message is supported. + +```ts +const { data: message, errors } = await chat.sendMessage({ + content: [ + { + text: 'describe the image in detail', + }, + { + image: { + format: 'png', + source: { + bytes: new Uint8Array([1, 2, 3]), + }, + }, + }, + ], +}); +``` + +#### AI context + +The `aiContext` argument allows you to optionally attach arbitrary data to the message. This is useful for passing additional information, like user information or current state of your application, in a user message to the AI assistant. + +```ts +const { data: message, errors } = await chat.sendMessage('Hello, world!', { + aiContext: { + user: { + name: 'Ian', + }, + }, +}); +``` + +#### Tool Configuration + +The `toolConfiguration` argument allows you to optionally pass a client tool configuration to the AI assistant with a user message. See the [Tools concept page](/[platform]/ai/concepts/tools) and [Tools guide](/[platform]/ai/conversation/tools/) for more information on how tools works. + + + Client tools are conceptually the same as data tools and lambda executable tools. They are API definitions provided to an LLM alongside a user message. The LLM can use the provided tool configuration to decide which tool (if any) to call in order to better respond to the user. However, there's an important distinction with client tools -- you are responsible for implementing the tool execution logic and responding to the AI assistant with the tool's response. + + +The `json` property is simply a JSON Schema definition of the tool's input. The AI assistant will use this schema to provide the expected input to your tool. +```ts +const { data: message, errors } = await conversation.sendMessage({ + content: [ + { + text: "I'd like to make a chocolate cake for my friend with a gluten intolerance. What ingredients do I need?", + }, + ], + toolConfiguration: { + tools: { + generateRecipe: { + description: "List ingredients needed for a recipe", + inputSchema: { + json: { + type: "object", + properties: { + ingredients: { + type: "array", + items: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, +}); +``` + +The response from the AI assistant will be a JSON object that matches the `inputSchema` definition. See [Subscribe to assistant responses](#subscribe-to-assistant-responses) for more information on how to handle the response. + +### Subscribe to assistant responses + +Assistant responses are streamed back to the client as they are generated. This allows for a more natural conversation flow where the user doesn't have to wait for a complete response from the AI assistant to see progress and begin reading the response. To subscribe to assistant responses, call the `.onStreamEvent()` method on your conversation instance. + +```ts +const subscription = conversation.onStreamEvent({ + next: (event) => { + console.log(event); + }, + error: (error) => { + console.error(error); + }, +}); + +// later... +subscription.unsubscribe(); +``` + +`onStreamEvent()` takes two callback functions as arguments: `next` and `error`. The `next` callback is invoked with each assistant response. + +The `error` callback is invoked if there's an error while processing messages. + +The `next` callback is invoked with a `ConversationStreamEvent` object. This type is accessible via `Schema['myChat']['streamEventType']` and is a union of the following types: + +#### ConversationStreamTextEvent + +As text is streamed back to the client, the `next` callback is invoked with a `ConversationStreamTextEvent` object. + +| Property | Type | Description | +|----------|:-----|:------------| +| `id` | `string` | The unique identifier for the stream event | +| `conversationId` | `string` | The ID of the conversation this event belongs to | +| `associatedUserMessageId` | `string` | The ID of the user message that triggered this response | +| `contentBlockIndex` | `number` | The index of the content block being streamed | +| `contentBlockDeltaIndex` | `number` | The index of the delta within the content block | +| `text` | `string` | The text content being streamed | + +#### ConversationStreamDoneAtIndexEvent + +When the AI assistant completes a content block, the `next` callback is invoked with a `ConversationStreamDoneAtIndexEvent` object. + +| Property | Type | Description | +|----------|:-----|:------------| +| `id` | `string` | The unique identifier for the stream event | +| `conversationId` | `string` | The ID of the conversation this event belongs to | +| `associatedUserMessageId` | `string` | The ID of the user message that triggered this response | +| `contentBlockIndex` | `number` | The index of the content block that is complete | +| `contentBlockDoneAtIndex` | `number` | The index at which the content block is complete | + + +#### ConversationStreamTurnDoneEvent + +When the AI assistant completes a turn, the `next` callback is invoked with a `ConversationStreamTurnDoneEvent` object. This event indicates that the assistant has completed a turn and is waiting for the next user message. + +| Property | Type | Description | +|----------|:-----|:------------| +| `id` | `string` | The unique identifier for the stream event | +| `conversationId` | `string` | The ID of the conversation this event belongs to | +| `associatedUserMessageId` | `string` | The ID of the user message that triggered this response | +| `contentBlockIndex` | `number` | The index of the final content block for the turn | +| `stopReason` | `string` | The reason why the assistant stopped generating the response | + +#### ConversationStreamToolUseEvent + +When the AI assistant uses a client tool, the `next` callback is invoked with a `ConversationStreamToolUseEvent` object. Tool use events are accumulated in your cloud resources and sent to the client as a single event. + +| Property | Type | Description | +|----------|:-----|:------------| +| `id` | `string` | The unique identifier for the stream event | +| `conversationId` | `string` | The ID of the conversation this event belongs to | +| `associatedUserMessageId` | `string` | The ID of the user message that triggered this response | +| `contentBlockIndex` | `number` | The index of the content block being streamed | +| `toolUse` | `ToolUseBlock` | The tool use block containing function call information | + + + **A note on ordering** + + There are no guarantees that events will be received by the client in order. For example, a `ConversationStreamTextEvent` with `contentBlockDeltaIndex` of `1` may be received before the preceding text with `contentBlockDeltaIndex` of `0`. Assume that events may be received out of order and use the `contentBlockIndex` and `contentBlockDeltaIndex` properties to order the events as needed. + + +### List messages for a conversation + +Retrieve all messages for a conversation by calling the `.listMessages()` method on your conversation instance. Recall that messages are automatically persisted, so you can retrieve them at any time to display the conversation history. + +```ts +const { data: messages, errors } = await conversation.listMessages(); +``` + +Similar to the `client.conversations.chat.list()` method, retrieved messages are paginated. Use the `nextToken` value to paginate through messages and optionally specify a `limit` to limit the number of messages returned. + +```ts +const { data: messages, errors } = await conversation.listMessages({ + limit: 10, + nextToken: '...', +}); +``` diff --git a/src/pages/[platform]/ai/conversation/knowledge-base/index.mdx b/src/pages/[platform]/ai/conversation/knowledge-base/index.mdx index 0ee336af625..ff7deee5f91 100644 --- a/src/pages/[platform]/ai/conversation/knowledge-base/index.mdx +++ b/src/pages/[platform]/ai/conversation/knowledge-base/index.mdx @@ -62,7 +62,7 @@ const schema = a.schema({ // highlight-end chat: a.conversation({ - aiModel: a.ai.model("Claude 3.5 Sonnet"), + aiModel: a.ai.model("Claude 3.5 Haiku"), systemPrompt: `You are a helpful assistant.`, // highlight-start tools: [ @@ -115,6 +115,7 @@ import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource'; import { data } from './data/resource'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import * as cdk from 'aws-cdk-lib'; const backend = defineBackend({ auth, @@ -124,7 +125,7 @@ const backend = defineBackend({ const KnowledgeBaseDataSource = backend.data.resources.graphqlApi.addHttpDataSource( "KnowledgeBaseDataSource", - `https://bedrock-runtime.${cdk.Stack.of(backend.data).region}.amazonaws.com`, + `https://bedrock-agent-runtime.${cdk.Stack.of(backend.data).region}.amazonaws.com`, { authorizationConfig: { signingRegion: cdk.Stack.of(backend.data).region, diff --git a/src/pages/[platform]/ai/conversation/tools/index.mdx b/src/pages/[platform]/ai/conversation/tools/index.mdx index 2a67d927288..56fc9ee2524 100644 --- a/src/pages/[platform]/ai/conversation/tools/index.mdx +++ b/src/pages/[platform]/ai/conversation/tools/index.mdx @@ -29,24 +29,41 @@ export function getStaticProps(context) { } +Tools allow LLMs to query information to respond with current and relevant information. They are invoked only if the LLM requests to use one based on the user's message and the tool's description. -Tools allow LLMs to take action or query information so it can respond with up to date information. There are a few different ways to define LLM tools in the Amplify AI kit. +There are a few different ways to define LLM tools in the Amplify AI kit. 1. Model tools 2. Query tools 3. Lambda tools -The easiest way you can define tools for the LLM to use is with data models and custom queries in your data schema. When you define tools in your data schema, Amplify will take care of all of the heavy lifting required to properly implement such as: +The easiest way to define tools for your conversation route is with `a.ai.dataTool()` for data models and custom queries in your data schema. When you define a tool for a conversation route, Amplify takes care of the heavy lifting: -* **Describing the tools to the LLM:** because each tool is a custom query or data model that is defined in the schema, Amplify knows the input shape needed for that tool -* **Invoking the tool with the right parameters:** after the LLM responds it wants to call a tool, the code that initially called the LLM needs to then run that code. -* **Maintaining the caller identity and authorization:** we don't want users to have access to more data through the LLM than they normally would, so when the LLM wants to invoke a tool we will call it with the user's identity. For example, if the LLM wanted to invoke a query to list Todos, it would only return the todos of the user and not everyone's todos. +* **Describing the tools to the LLM:** Each tool definition is an Amplify model query or custom query that is defined in the schema. Amplify knows the input parameters needed for that tool and describes them to the LLM. +* **Invoking the tool with the right parameters:** After the LLM requests to use a tool with necessary input parameters, the conversation handler Lambda function invokes the tool, returns the result to the LLM, and continues the conversation. +* **Maintaining the caller identity and authorization:** Through tools, the LLM can only access data that the application user has access to. When the LLM requests to invoke a tool, we will call it with the user's identity. For example, if the LLM wanted to invoke a query to list Todos, it would only return the todos that user has access to. ## Model tools -You can give the LLM access to your data models by referencing them in an `a.ai.dataTool()` with a reference to a model in your data schema. +You can give the LLM access to your data models by referencing them in an `a.ai.dataTool()` with a reference to a model in your data schema. This requires that the model uses at least one of the following authorization strategies: + +**[Per user data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/per-user-per-owner-data-access/)** +- `owner()` +- `ownerDefinedIn()` +- `ownersDefinedIn()` + +**[Any signed-in user data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/signed-in-user-data-access/)** +- `authenticated()` + +**[Per user group data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/user-group-based-data-access/)** +- `group()` +- `groupsDefinedIn()` +- `groups()` +- `groupsDefinedIn()` + +```ts title="amplify/data/resource.ts" +import { type ClientSchema, a, defineData } from "@aws-amplify/backend"; -```ts const schema = a.schema({ Post: a.model({ title: a.string(), @@ -55,13 +72,18 @@ const schema = a.schema({ .authorization(allow => allow.owner()), chat: a.conversation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'Hello, world!', tools: [ a.ai.dataTool({ + // The name of the tool as it will be referenced in the message to the LLM name: 'PostQuery', + // The description of the tool provided to the LLM. + // Use this to help the LLM understand when to use the tool. description: 'Searches for Post records', + // A reference to the `a.model()` that the tool will use model: a.ref('Post'), + // The operation to perform on the model modelOperation: 'list', }), ], @@ -71,26 +93,29 @@ const schema = a.schema({ This will let the LLM list and filter `Post` records. Because the data schema has all the information about the shape of a `Post` record, the data tool will provide that information to the LLM so you don't have to. Also, the Amplify AI kit handles authorizing the tool use requests based on the caller's identity. This means if you have an owner-based model, the LLM will only be able to query the user's records. -*The only supported model operation currently is 'list'* + + +The only supported model operation is `'list'`. + + ## Query tools -You can also give the LLM access to custom queries. You can define a custom query with a [Function](/[platform]/build-a-backend/functions/set-up-function/) handler and then reference that custom query as a tool. +You can also give the LLM access to custom queries defined in your data schema. To do so, define a custom query with a [function or custom handler](https://docs.amplify.aws/react/build-a-backend/data/custom-business-logic/) and then reference that custom query as a tool. This requires that the custom query uses the `allow.authenticated()` authorization strategy. ```ts title="amplify/data/resource.ts" -// highlight-start import { type ClientSchema, a, defineData, defineFunction } from "@aws-amplify/backend"; -// highlight-end -// highlight-start export const getWeather = defineFunction({ name: 'getWeather', - entry: 'getWeather.ts' + entry: './getWeather.ts', + environment: { + API_ENDPOINT: 'MY_API_ENDPOINT', + API_KEY: secret('MY_API_KEY'), + }, }); -// highlight-end const schema = a.schema({ - // highlight-start getWeather: a.query() .arguments({ city: a.string() }) .returns(a.customType({ @@ -99,68 +124,73 @@ const schema = a.schema({ })) .handler(a.handler.function(getWeather)) .authorization((allow) => allow.authenticated()), - // highlight-end chat: a.conversation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant', - // highlight-start tools: [ a.ai.dataTool({ - name: 'getWeather', + // The name of the tool as it will be referenced in the LLM prompt + name: 'get_weather', + // The description of the tool provided to the LLM. + // Use this to help the LLM understand when to use the tool. description: 'Gets the weather for a given city', + // A reference to the `a.query()` that the tool will invoke. query: a.ref('getWeather'), }), ] - // highlight-end - }), + }) + .authorization((allow) => allow.owner()), }); ``` -Because the definition of the query itself has the shape of the inputs and outputs (arguments and returns), the Amplify data tool can automatically tell the LLM exactly how to call the custom query. +The Amplify data tool takes care of specifying the necessary input parameters to the LLM based on the query definition. - - -The description of the tool is very important to help the LLM know when to use that tool. The more descriptive you are about what the tool does, the better. - - - -Here is an example Lambda function handler for our `getWeather` query: +Below is an illustrative example of a Lambda function handler for the `getWeather` query. ```ts title="amplify/data/getWeather.ts" +import { env } from "$amplify/env/getWeather"; import type { Schema } from "./resource"; export const handler: Schema["getWeather"]["functionHandler"] = async ( event ) => { - // This returns a mock value, but you can connect to any API, database, or other service - return { - value: 42, - unit: 'C' - }; + const { city } = event.arguments; + if (!city) { + throw new Error('City is required'); + } + + const url = `${env.API_ENDPOINT}?city=${encodeURIComponent(city)}`; + const request = new Request(url, { + headers: { + Authorization: `Bearer ${env.API_KEY}` + } + }); + + const response = await fetch(request); + const weather = await response.json(); + return weather; } ``` Lastly, you will need to update your **`amplify/backend.ts`** file to include the newly defined `getWeather` function. ```ts title="amplify/backend.ts" -// highlight-start -import { getWeather } from "./data/resource"; -// highlight-end +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { data, getWeather } from './data/resource'; const backend = defineBackend({ auth, data, - // highlight-start getWeather - // highlight-end }); ``` -## Connect to any AWS Service +### Connect to any AWS Service -You can connect to any AWS service by defining a custom query and calling that service in the function handler. Then you will need to provide the Lambda with the proper permissions to call the AWS service. +You can connect to any AWS service by defining a custom query and calling that service in the function handler. To properly authorize the custom query function to call the AWS service, you will need to provide the Lambda with the proper permissions. ```ts title="amplify/backend.ts" import { defineBackend } from "@aws-amplify/backend"; @@ -185,80 +215,118 @@ backend.getWeather.resources.lambda.addToRolePolicy( ) ``` +## Custom Lambda Tools +You can also define a tool that executes in the conversation handler AWS Lambda function. This is useful if you want to define a tool that is not related to your data schema or that does simple tasks within the Lambda function runtime. -## Custom Lambda Tools +First install the `@aws-amplify/backend-ai` package. -Conversation routes can also have completely custom tools defined in a Lambda handler. +```bash title="Terminal" +npm install @aws-amplify/backend-ai +``` -### Create a custom conversation handler function +Define a custom conversation handler function in your data schema and reference the function in the `handler` property of the `a.conversation()` definition. ```ts title="amplify/data/resource.ts" import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; import { defineConversationHandlerFunction } from '@aws-amplify/backend-ai/conversation'; -const chatHandler = defineConversationHandlerFunction({ +export const chatHandler = defineConversationHandlerFunction({ entry: './chatHandler.ts', name: 'customChatHandler', models: [ - { modelId: a.ai.model("Claude 3 Haiku") } + { modelId: a.ai.model("Claude 3.5 Haiku") } ] }); const schema = a.schema({ chat: a.conversation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: "You are a helpful assistant", handler: chatHandler, }) + .authorization((allow) => allow.owner()), }) ``` -### Implement the custom handler +Define the executable tool(s) and handler. Below is an illustrative example of a custom conversation handler function that defines a `calculator` tool. ```ts title="amplify/data/chatHandler.ts" - import { - ConversationTurnEvent, - ExecutableTool, - handleConversationTurnEvent, -} from '@aws-amplify/ai-constructs/conversation/runtime'; -import { ToolResultContentBlock } from '@aws-sdk/client-bedrock-runtime'; - -const thermometer: ExecutableTool = { - name: 'thermometer', - description: 'Returns current temperature in a city', - execute: (input): Promise => { - if (input.city === 'Seattle') { - return Promise.resolve({ - text: `75F`, - }); - } - return Promise.resolve({ - text: 'unknown' - }) - }, - inputSchema: { - json: { - type: 'object', - 'properties': { - 'city': { - 'type': 'string', - 'description': 'The city name' - } + ConversationTurnEvent, + createExecutableTool, + handleConversationTurnEvent +} from '@aws-amplify/backend-ai/conversation/runtime'; + +const jsonSchema = { + json: { + type: 'object', + properties: { + 'operator': { + 'type': 'string', + 'enum': ['+', '-', '*', '/'], + 'description': 'The arithmetic operator to use' }, - required: ['city'] - } + 'operands': { + 'type': 'array', + 'items': { + 'type': 'number' + }, + 'minItems': 2, + 'maxItems': 2, + 'description': 'Two numbers to perform the operation on' + } + }, + required: ['operator', 'operands'] } -}; +} as const; +// declare as const to allow the input type to be derived from the JSON schema in the tool handler definition. + +const calculator = createExecutableTool( + 'calculator', + 'Returns the result of a simple calculation', + jsonSchema, + // input type is derived from the JSON schema + (input) => { + const [a, b] = input.operands; + switch (input.operator) { + case '+': return Promise.resolve({ text: (a + b).toString() }); + case '-': return Promise.resolve({ text: (a - b).toString() }); + case '*': return Promise.resolve({ text: (a * b).toString() }); + case '/': + if (b === 0) throw new Error('Division by zero'); + return Promise.resolve({ text: (a / b).toString() }); + default: + throw new Error('Invalid operator'); + } + }, +); -/** - * Handler with simple tool. - */ export const handler = async (event: ConversationTurnEvent) => { await handleConversationTurnEvent(event, { - tools: [thermometer], + tools: [calculator], }); }; ``` +Note that we throw an error in the `calculator` tool example above if the input is invalid. This error is surfaced to the LLM by the conversation handler function. Depending on the error message, the LLM may try to use the tool again with different input or completing its response with test for the user. + +Lastly, update your backend definition to include the newly defined `chatHandler` function. + +```ts title="amplify/backend.ts" +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { data, chatHandler } from './data/resource'; + +defineBackend({ + auth, + data, + chatHandler, +}); +``` + +### Best Practices + +- Validate and sanitize any input from the LLM before using it in your application, e.g. don't use it directly in a database query or use `eval()` to execute it. +- Handle errors gracefully and provide meaningful error messages. +- Log and monitor tool usage to detect potential misuse or issues. diff --git a/src/pages/[platform]/ai/generation/data-extraction/index.mdx b/src/pages/[platform]/ai/generation/data-extraction/index.mdx new file mode 100644 index 00000000000..e1b63085a21 --- /dev/null +++ b/src/pages/[platform]/ai/generation/data-extraction/index.mdx @@ -0,0 +1,117 @@ +import { getCustomStaticPath } from "@/utils/getCustomStaticPath"; + +export const meta = { + title: "Data Extraction", + description: + "How to extract data from unstructured text.", + platforms: [ + "javascript", + "react-native", + "angular", + "nextjs", + "react", + "vue", + ], +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta, + showBreadcrumbs: false, + }, + }; +} + +Data extraction allows you to parse unstructured text and extract structured data using AI. This is useful for converting free-form text into typed objects that can be used in your application. + +The following example shows how to extract product details from an unstructured product description. The AI model will analyze the text and return a structured object containing the product name, summary, price, and category. + +```typescript title="amplify/data/resource.ts" +const schema = a.schema({ + ProductDetails: a.customType({ + name: a.string().required(), + summary: a.string().required(), + price: a.float().required(), + category: a.string().required(), + }), + + extractProductDetails: a.generation({ + aiModel: a.ai.model('Claude 3.5 Haiku'), + systemPrompt: 'Extract the property details from the text provided', + }) + .arguments({ + productDescription: a.string() + }) + .returns(a.ref('ProductDetails')) + .authorization((allow) => allow.authenticated()), +}); +``` + + +```ts title="Data Client Request" +import type { Schema } from "../amplify/data/resource"; +import { generateClient } from "aws-amplify/api"; + +export const client = generateClient(); + +const productDescription = `The NBA Official Game Basketball is a premium +regulation-size basketball crafted with genuine leather and featuring +official NBA specifications. This professional-grade ball offers superior grip +and durability, with deep channels and a moisture-wicking surface that ensures +consistent performance during intense game play. Priced at $159.99, this high-end +basketball belongs in our Professional Sports Equipment category and is the same model +used in NBA games.` + +const { data, errors } = await client.generations + .extractProductDetails({ productDescription }) + +/** +Example response: +{ + "name": "NBA Official Game Basketball", + "summary": "Premium regulation-size NBA basketball made with genuine leather. Features official NBA specifications, superior grip, deep channels, and moisture-wicking surface for consistent game play performance.", + "price": 159.99, + "category": "Professional Sports Equipment" +} +*/ +``` + + + + + +```ts title="src/components/Example.tsx" +import type { Schema } from "../amplify/data/resource"; +import { generateClient } from "aws-amplify/api"; +import { createAIHooks } from "@aws-amplify/ui-react-ai"; + +const client = generateClient({ authMode: "userPool" }); +const { useAIGeneration } = createAIHooks(client); + +export default function Example() { + const productDescription = `The NBA Official Game Basketball is a premium + regulation-size basketball crafted with genuine leather and featuring + official NBA specifications. This professional-grade ball offers superior grip + and durability, with deep channels and a moisture-wicking surface that ensures + consistent performance during intense game play. Priced at $159.99, this high-end + basketball belongs in our Professional Sports Equipment category and is the same model + used in NBA games.` + + // data is React state and will be populated when the generation is returned + const [{ data, isLoading }, extractProductDetails] = + useAIGeneration("extractProductDetails"); + + const productDetails = async () => { + extractProductDetails({ + productDescription + }); + }; +} +``` + diff --git a/src/pages/[platform]/ai/generation/index.mdx b/src/pages/[platform]/ai/generation/index.mdx index cbbc22fc943..674e6bf4e43 100644 --- a/src/pages/[platform]/ai/generation/index.mdx +++ b/src/pages/[platform]/ai/generation/index.mdx @@ -41,7 +41,7 @@ Under the hood, a generation route is an AWS AppSync query that ensures the AI m ```ts title="Schema Definition" const schema = a.schema({ generateRecipe: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates recipes.', }) .arguments({ description: a.string() }) @@ -117,7 +117,7 @@ export default function Example() { ```ts title="Schema Definition" const schema = ({ summarize: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'Provide an accurate, clear, and concise summary of the input provided' }) .arguments({ input: a.string() }) @@ -141,7 +141,7 @@ This ability to control the randomness and diversity of responses is useful for ```ts title="Inference Parameters" const schema = a.schema({ generateHaiku: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates haikus.', // highlight-start inferenceConfiguration: { @@ -168,7 +168,7 @@ const schema = a.schema({ instructions: a.string(), }), generateRecipe: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates recipes.', }) .arguments({ description: a.string() }) @@ -187,7 +187,7 @@ const schema = a.schema({ instructions: a.string(), }), generateRecipe: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates recipes.', }) .arguments({ description: a.string() }) @@ -211,7 +211,7 @@ The following AppSync scalar types are not supported as **required** fields in r ```ts title="Unsupported Required Type" const schema = a.schema({ generateUser: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates users.', }) .arguments({ description: a.string() }) diff --git a/src/pages/[platform]/ai/index.mdx b/src/pages/[platform]/ai/index.mdx index edee3b692a2..efc70d6d257 100644 --- a/src/pages/[platform]/ai/index.mdx +++ b/src/pages/[platform]/ai/index.mdx @@ -4,6 +4,7 @@ import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; export const meta = { title: 'AI kit', description: 'The quickest way for fullstack developers to build web apps with AI capabilities such as chat, conversational search, and summarization', + isNew: true, route: '/[platform]/ai', platforms: [ 'angular', diff --git a/src/pages/[platform]/ai/set-up-ai/index.mdx b/src/pages/[platform]/ai/set-up-ai/index.mdx index 1bd3860dbcd..7526472567a 100644 --- a/src/pages/[platform]/ai/set-up-ai/index.mdx +++ b/src/pages/[platform]/ai/set-up-ai/index.mdx @@ -86,7 +86,7 @@ const schema = a.schema({ // This will add a new conversation route to your Amplify Data backend. // highlight-start chat: a.conversation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant', }) .authorization((allow) => allow.owner()), @@ -95,7 +95,7 @@ const schema = a.schema({ // This adds a new generation route to your Amplify Data backend. // highlight-start generateRecipe: a.generation({ - aiModel: a.ai.model('Claude 3 Haiku'), + aiModel: a.ai.model('Claude 3.5 Haiku'), systemPrompt: 'You are a helpful assistant that generates recipes.', }) .arguments({ @@ -381,8 +381,17 @@ export default function App() { ```tsx title="pages/index.tsx" -import { Flex, TextAreaField, Loader, Text, View } from "@aws-amplify/ui-react" -import { useAIConversation } from "@/client"; +import { useAIGeneration } from "@/client"; +import { + Button, + Flex, + Heading, + Loader, + Text, + TextAreaField, + View, +} from "@aws-amplify/ui-react"; +import React from "react"; export default function Page() { const [description, setDescription] = React.useState(""); @@ -429,8 +438,18 @@ export default function Page() { ```tsx title="app/page.tsx" -'use client' -import { useAIConversation } from "@/client"; +"use client"; +import { useAIGeneration } from "@/client"; +import { + Button, + Flex, + Heading, + Loader, + Text, + TextAreaField, + View, +} from "@aws-amplify/ui-react"; +import React from "react"; export default function Page() { const [description, setDescription] = React.useState(""); diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/existing-resources/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/existing-resources/index.mdx index c34c2762f6f..29e57829bba 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/existing-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/existing-resources/index.mdx @@ -53,11 +53,15 @@ Alternatively, you can configure the client library directly using `Amplify.conf ```ts title="src/main.ts" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; +import outputs from '../amplify_outputs.json'; + +const amplifyConfig = parseAmplifyConfig(outputs); Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Analytics: { - ...Amplify.getConfig().Analytics, + ...amplifyConfig.Analytics, Pinpoint: { // REQUIRED - Amazon Pinpoint App Client ID appId: 'XXXXXXXXXXabcdefghij1234567890ab', diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/personalize-recommendations/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/personalize-recommendations/index.mdx index e71f032ca08..546d5280333 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/personalize-recommendations/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/personalize-recommendations/index.mdx @@ -57,8 +57,12 @@ Configure Amazon Personalize: ```javascript title="src/index.js" import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; + +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Analytics: { Personalize: { // REQUIRED - The trackingId to track the events diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/set-up-analytics/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/set-up-analytics/index.mdx index 2577a83d329..042913167c4 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/set-up-analytics/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/set-up-analytics/index.mdx @@ -183,26 +183,18 @@ Import and load the configuration file in your app. It's recommended you add the ```js title="src/index.js" import { Amplify } from 'aws-amplify'; -import { record } from 'aws-amplify/analytics'; import outputs from '../amplify_outputs.json'; -Amplify.configure({ - ...Amplify.getConfig(), - Analytics: amplifyconfig.Analytics, -}); +Amplify.configure(outputs); ``` ```js title="pages/_app.tsx" import { Amplify } from 'aws-amplify'; -import { record } from 'aws-amplify/analytics'; import outputs from '@/amplify_outputs.json'; -Amplify.configure({ - ...Amplify.getConfig(), - Analytics: amplifyconfig.Analytics, -}); +Amplify.configure(outputs); ``` diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/storing-data/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/storing-data/index.mdx index fb8e5bbe426..90d1425b011 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/storing-data/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/storing-data/index.mdx @@ -128,9 +128,12 @@ Configure Firehose: ```javascript title="src/index.js" import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; + +const amplifyConfig = parseAmplifyConfig(outputs); Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Analytics: { KinesisFirehose: { // REQUIRED - Amazon Kinesis Firehose service region diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/streaming-data/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/streaming-data/index.mdx index d64b0676c7b..2ded8bd5ad4 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/analytics/streaming-data/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/analytics/streaming-data/index.mdx @@ -93,9 +93,12 @@ Configure Kinesis: ```javascript title="src/index.js" // Configure the plugin after adding it to the Analytics module import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; + +const amplifyConfig = parseAmplifyConfig(outputs); Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Analytics: { Kinesis: { // REQUIRED - Amazon Kinesis service region diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/deletion-backup-resources/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/deletion-backup-resources/index.mdx index d1c468ed93e..4bdc9097752 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/deletion-backup-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/deletion-backup-resources/index.mdx @@ -164,6 +164,12 @@ plan.addSelection("BackupPlanSelection", { For example, if you would like to retain a resource on stack deletion, you can use the `applyRemovalPolicy` property on the resource to add a retention policy. + + +`ampx sandbox delete` ignores any resource removal policy and always deletes all resources. + + + ```ts title="amplify/backend.ts" import { defineBackend } from "@aws-amplify/backend"; import { auth } from "./auth/resource"; diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/geo/existing-resources/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/geo/existing-resources/index.mdx index b45d8ae362d..2b539770fe1 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/geo/existing-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/geo/existing-resources/index.mdx @@ -101,15 +101,17 @@ There are two roles created by Cognito: an "authenticated role" that grants sign ## Configure client library directly -You can first import and configure the generated `amplify_outputs.json`. You can then manually configure Amplify Geo like this: +You can first import and parse the generated `amplify_outputs.json`. You can then manually configure Amplify Geo like this: ```js import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '../amplify_outputs.json'; -Amplify.configure(outputs); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Geo: { LocationService: { maps: { diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/geo/geofences/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/geo/geofences/index.mdx index 0a170a90b08..658cdc2d239 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/geo/geofences/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/geo/geofences/index.mdx @@ -106,7 +106,7 @@ export default withAuthenticator(Map); -**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. +**Note:** When using the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) you can use the [`useControl` hook from react-map-gl](https://visgl.github.io/react-map-gl/docs/api-reference/maplibre/use-control) to render the Geofence control component. The [react-map-gl](https://visgl.github.io/react-map-gl) dependency is already installed through `@aws-amplify/ui-react-geo`, you do not need to install it manually. ```javascript import React from 'react'; diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/in-app-messaging/integrate-application/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/in-app-messaging/integrate-application/index.mdx index 4cd767ad511..9b2259a5f30 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/in-app-messaging/integrate-application/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/in-app-messaging/integrate-application/index.mdx @@ -56,7 +56,7 @@ Learn more about Amplify In-App Messaging UI and how to fully unlock its capabil ```bash title="Terminal" showLineNumbers={false} -npm add @aws-amplify/ui-react-native react-native-safe-area-context +npm add @aws-amplify/ui-react-native react-native-safe-area-context@^4.2.5 ``` diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/interactions/set-up-interactions/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/interactions/set-up-interactions/index.mdx index ee19f87cb82..afaf411badc 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/interactions/set-up-interactions/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/interactions/set-up-interactions/index.mdx @@ -101,11 +101,13 @@ Import and load the configuration file in your app. It's recommended you add the ```javascript title="src/index.js" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '../amplify_outputs.json'; -Amplify.configure(outputs); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...Amplify.getConfig(), + ...amplifyConfig, Interactions: { LexV2: { '': { diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/set-up-predictions/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/set-up-predictions/index.mdx index 9b7642739dd..25b37e8b083 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/set-up-predictions/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/set-up-predictions/index.mdx @@ -145,7 +145,7 @@ backend.addOutput({ To install the Amplify library to use predictions features, run the following commands in your project's root folder: ```bash title="Terminal" showLineNumbers={false} -npm add aws-amplify +npm add aws-amplify @aws-amplify/predictions ``` ## Configure the frontend @@ -153,13 +153,8 @@ npm add aws-amplify Import and load the configuration file in your app. It is recommended you add the Amplify configuration step to your app's root entry point. For example `main.ts` in React and Angular. ```ts title="src/main.ts" -import { Predictions } from "aws-amplify/predictions"; -import outputs from "./amplify_outputs.json"; +import { Amplify } from "aws-amplify"; +import outputs from '../amplify_outputs.json'; Amplify.configure(outputs); - -Amplify.configure({ - ...Amplify.getConfig(), - Predictions: config.custom.Predictions, -}); ``` diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/existing-resources/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/existing-resources/index.mdx index 6f06d781a2c..745f49dce32 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/existing-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/existing-resources/index.mdx @@ -26,22 +26,22 @@ export function getStaticProps(context) { }; } -Existing Amazon API Gateway resources can be used with the Amplify Libraries by calling `Amplify.configure()` with the API Gateway API name and options. Note, you will need to supply the full resource configuration and library options objects when calling `Amplify.configure()`. The following example shows how to configure additional API Gateway resources to an existing Amplify application: +Existing Amazon API Gateway resources can be used with the Amplify Libraries by calling `Amplify.configure()` with the API Gateway API name and options. Note, you will need to parse the Amplify configuration using `parseAmplifyConfig` before calling `Amplify.configure()`. The following example shows how to configure additional API Gateway resources to an existing Amplify application: ```ts import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '../amplify_outputs.json'; -Amplify.configure(outputs): -const existingConfig = Amplify.getConfig(); +const amplifyConfig = parseAmplifyConfig(outputs); // Add existing resource to the existing configuration. Amplify.configure({ - ...existingConfig, + ...amplifyConfig, API: { - ...existingConfig.API, + ...amplifyConfig.API, REST: { - ...existingConfig.API?.REST, + ...amplifyConfig.API?.REST, YourAPIName: { endpoint: 'https://abcdefghij1234567890.execute-api.us-east-1.amazonaws.com/stageName', diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-http-api/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-http-api/index.mdx index d468d6e8a3f..cd4e0d0d0cc 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-http-api/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-http-api/index.mdx @@ -228,14 +228,15 @@ Import and load the configuration file in your app. It's recommended you add the ```ts title="src/main.ts" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '../amplify_outputs.json'; -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...existingConfig, + ...amplifyConfig, API: { - ...existingConfig.API, + ...amplifyConfig.API, REST: outputs.custom.API, }, }); @@ -245,14 +246,15 @@ Amplify.configure({ ```tsx title="pages/_app.tsx" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '@/amplify_outputs.json'; -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...existingConfig, + ...amplifyConfig, API: { - ...existingConfig.API, + ...amplifyConfig.API, REST: outputs.custom.API, }, }); diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-rest-api/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-rest-api/index.mdx index e0a9ab8a879..834ec6f070f 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-rest-api/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/set-up-rest-api/index.mdx @@ -211,14 +211,15 @@ Import and load the configuration file in your app. It's recommended you add the ```javascript title="src/main.ts" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '../amplify_outputs.json'; -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...existingConfig, + ...amplifyConfig, API: { - ...existingConfig.API, + ...amplifyConfig.API, REST: outputs.custom.API, }, }); @@ -228,14 +229,15 @@ Amplify.configure({ ```tsx title="pages/_app.tsx" import { Amplify } from 'aws-amplify'; +import { parseAmplifyConfig } from "aws-amplify/utils"; import outputs from '@/amplify_outputs.json'; -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); +const amplifyConfig = parseAmplifyConfig(outputs); + Amplify.configure({ - ...existingConfig, + ...amplifyConfig, API: { - ...existingConfig.API, + ...amplifyConfig.API, REST: outputs.custom.API, }, }); diff --git a/src/pages/[platform]/build-a-backend/auth/advanced-workflows/index.mdx b/src/pages/[platform]/build-a-backend/auth/advanced-workflows/index.mdx index e365cf82809..051b6bb61e1 100644 --- a/src/pages/[platform]/build-a-backend/auth/advanced-workflows/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/advanced-workflows/index.mdx @@ -396,7 +396,7 @@ import { } from 'aws-amplify/auth'; // Note: This example requires installing `@aws-sdk/client-cognito-identity` to obtain Cognito credentials -// npm i @aws-sdk/client-cognito-identity +// npm add @aws-sdk/client-cognito-identity import { CognitoIdentity } from '@aws-sdk/client-cognito-identity'; // You can make use of the sdk to get identityId and credentials diff --git a/src/pages/[platform]/build-a-backend/auth/concepts/external-identity-providers/index.mdx b/src/pages/[platform]/build-a-backend/auth/concepts/external-identity-providers/index.mdx index 4090850b93e..74d1c9d40dd 100644 --- a/src/pages/[platform]/build-a-backend/auth/concepts/external-identity-providers/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/concepts/external-identity-providers/index.mdx @@ -116,11 +116,14 @@ Secrets must be created manually with [`ampx sandbox secret`](/[platform]/refere + + ```ts title="amplify/auth/resource.ts" import { defineAuth, secret } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { + email: true, externalProviders: { google: { clientId: secret('GOOGLE_CLIENT_ID'), @@ -150,6 +153,43 @@ export const auth = defineAuth({ }); ``` + + + +```ts title="amplify/auth/resource.ts" +import { defineAuth, secret } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + google: { + clientId: secret('GOOGLE_CLIENT_ID'), + clientSecret: secret('GOOGLE_CLIENT_SECRET') + }, + signInWithApple: { + clientId: secret('SIWA_CLIENT_ID'), + keyId: secret('SIWA_KEY_ID'), + privateKey: secret('SIWA_PRIVATE_KEY'), + teamId: secret('SIWA_TEAM_ID') + }, + loginWithAmazon: { + clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), + clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET') + }, + facebook: { + clientId: secret('FACEBOOK_CLIENT_ID'), + clientSecret: secret('FACEBOOK_CLIENT_SECRET') + }, + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + } + } +}); +``` + + + You need to now inform your external provider of the newly configured authentication resource and its OAuth redirect URI: @@ -216,21 +256,26 @@ You need to now inform your external provider of the newly configured authentica +[Learn more about using social identity providers with user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html) + ### Customizing scopes for retrieving user data from external providers You can determine the pieces of data you want to retrieve from each external provider when setting them up in the `amplify/auth/resource.ts` file using `scopes`. + + ```ts title="amplify/auth/resource.ts" import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { - externalAuthProviders: { + email: true, + externalProviders: { loginWithAmazon: { clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'), // highlight-next-line - scopes: ['email'] + scopes: ['profile'] }, callbackUrls: [ 'http://localhost:3000/profile', @@ -242,6 +287,31 @@ export const auth = defineAuth({ }); ``` + + + +```ts title="amplify/auth/resource.ts" +import { defineAuth } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + loginWithAmazon: { + clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), + clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'), + // highlight-next-line + scopes: ['email'] + }, + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + } + } +}); +``` + + + ### Attribute mapping You can map which attributes are mapped between your external identity provider and your users created in Cognito. We will be able to have the best level of protection for developers if we ensure that attribute mappings that would not work are called out by the type system. @@ -252,11 +322,14 @@ If you specify an attribute in your authentication resource as required, and it + + ```ts title="amplify/auth/resource.ts" import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { + email: true, externalAuthProviders: { loginWithAmazon: { clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), @@ -276,14 +349,46 @@ export const auth = defineAuth({ } }); ``` + + + + +```ts title="amplify/auth/resource.ts" +import { defineAuth } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + externalAuthProviders: { + loginWithAmazon: { + clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), + clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'), + // highlight-start + attributeMapping: { + email: 'email' + } + // highlight-end + }, + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + } + } +}); +``` + + + + -- [Learn more about configuring the React Authenticator component for external providers](https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration#external-providers) +[Learn more about configuring the React Authenticator component for external providers](https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration#external-providers) ## Configure OIDC provider To setup a OIDC provider, you can configure them in your `amplify/auth/resource.ts` file. For example, if you would like to setup a Microsoft EntraID provider, you can do so as follows: + + ```ts title="amplify/auth/resource.ts" import { defineAuth, secret } from '@aws-amplify/backend'; @@ -309,6 +414,34 @@ export const auth = defineAuth({ }); ``` + + + +```ts title="amplify/auth/resource.ts" +import { defineAuth, secret } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + oidc: [ + { + name: 'MicrosoftEntraID', + clientId: secret('MICROSOFT_ENTRA_ID_CLIENT_ID'), + clientSecret: secret('MICROSOFT_ENTRA_ID_CLIENT_SECRET'), + issuerUrl: '', + }, + ], + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + }, + }, +}); +``` + + + + Use the `signInWithRedirect` API to initiate sign-in with an OIDC identity provider. @@ -328,6 +461,8 @@ await signInWithRedirect({ To setup a SAML provider, you can configure them in your `amplify/auth/resource.ts` file. For example, if you would like to setup a Microsoft EntraID provider, you can do so as follows: + + ```ts title="amplify/auth/resource.ts" import { defineAuth } from '@aws-amplify/backend'; @@ -352,6 +487,33 @@ export const auth = defineAuth({ }); ``` + + + +```ts title="amplify/auth/resource.ts" +import { defineAuth } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + saml: { + name: 'MicrosoftEntraIDSAML', + metadata: { + metadataContent: '', // or content of the metadata file + metadataType: 'URL', // or 'FILE' + }, + }, + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + }, + }, +}); +``` + + + + Use the `signInWithRedirect` API to initiate sign-in with a SAML identity provider. @@ -472,7 +634,6 @@ import { signInWithRedirect } from 'aws-amplify/auth'; signInWithRedirect({ provider: 'Apple' }); - ``` ### Redirect URLs @@ -483,7 +644,6 @@ _Sign in_ & _Sign out_ redirect URL(s) are used to redirect end users after the If you have multiple sign out redirect URLs configured, you may choose to override the default behavior of selecting a redirect URL and provide the one of your choosing when calling `signOut`. The provided redirect URL should match at least one of the configured redirect URLs. If no redirect URL is provided to `signOut`, the first item from the the configured redirect URLs list that does not contain a HTTP nor HTTPS prefix will be picked. ```ts -import { Amplify } from 'aws-amplify'; import { signOut } from 'aws-amplify/auth'; // Assuming the following URLS were provided manually or via the Amplify configuration file, @@ -492,10 +652,9 @@ import { signOut } from 'aws-amplify/auth'; signOut({ global: false, oauth: { - redirectUrl: 'https://authProvider/logout?logout_uri=myDevApp://' + redirectUrl: 'https://authProvider/logout?logout_uri=myapp://' } }); - ``` Irrespective of whether a `redirectUrl` is provided to `signOut`, a URL that does not contain http or https is expected to be present in the configured redirect URL list. This is because iOS requires an appScheme when creating the web session. diff --git a/src/pages/[platform]/build-a-backend/auth/concepts/passwordless/index.mdx b/src/pages/[platform]/build-a-backend/auth/concepts/passwordless/index.mdx new file mode 100644 index 00000000000..6fc490e7ca0 --- /dev/null +++ b/src/pages/[platform]/build-a-backend/auth/concepts/passwordless/index.mdx @@ -0,0 +1,149 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Passwordless', + description: 'Learn how to configure passwordless sign-in flows', + platforms: [ + 'android', + 'angular', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue' + ] +}; + +export function getStaticPaths() { + return getCustomStaticPath(meta.platforms); +} + +export function getStaticProps() { + return { + props: { + meta + } + }; +} + +Amplify supports the use of passwordless authentication flows using the following methods: + +- [SMS-based one-time password (SMS OTP)](#sms-otp) +- [Email-based one-time password (Email OTP)](#email-otp) +- [WebAuthn passkey](#webauthn-passkey) + +Passwordless authentication removes the security risks and user friction associated with traditional passwords. +{/* add more color */} + + + +**Warning:** Passwordless configuration is currently not available in `defineAuth`. We are currently working towards enabling support for passwordless configurations. [Visit the GitHub issue to track the progress](https://github.com/aws-amplify/amplify-backend/issues/2276) + + + +Learn how to implement passwordless sign-in flows by [overriding the Cognito UserPool to enable the sign-in methods below](/[platform]/build-a-backend/auth/modify-resources-with-cdk/#override-cognito-userpool-to-enable-passwordless-sign-in-methods). + +{/* need a section about what a "preferred" factor is */} + +## SMS OTP + +SMS-based authentication uses phone numbers as the identifier and text messages as the verification channel. At a high level end users will perform the following steps to authenticate: + +1. User enters their phone number to sign up/sign in +2. They receive a text message with a time-limited code +3. After the user enters their code they are authenticated + +{/* quick blurb of basic usage */} + + +{/* */} + + + + +{/* */} + + + + + +{/* */} + + + + + +SMS-based one-time password requires your Amazon Cognito user pool to be configured to use Amazon Simple Notification Service (SNS) to send text messages. [Learn how to configure your auth resource with SNS](/[platform]/build-a-backend/auth/moving-to-production/#sms). + +{/* NOTE the linked page will need to be updated with sns instructions */} + + + +[Learn more about using SMS OTP in your application code](/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/#sms-otp). + +## Email OTP + +Email-based authentication uses email addresses for identification and verification. At a high level end users will perform the following steps to authenticate: + +1. User enters their email address to sign up/sign in +2. They receive an email message with a time-limited code +3. After the users enters their code they are authenticated + +{/* quick blurb of basic usage */} + + +{/* */} + + + + +{/* */} + + + + +{/* */} + + + + + +Email-based one-time password requires your Amazon Cognito user pool to be configured to use Amazon Simple Email Service (SES) to send email messages. [Learn how to configure your auth resource with SES](/[platform]/build-a-backend/auth/moving-to-production/#email). + + + +[Learn more about using email OTP in your application code](/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/#email-otp). + +## WebAuthn Passkey + +WebAuthn uses biometrics or security keys for authentication, leveraging device-specific security features. At a high level end users will perform the following steps to authenticate: + +1. User chooses to register a passkey +2. Their device prompts for biometric/security key verification +3. For future logins, they'll authenticate using the same method + +{/* quick blurb of basic usage */} + + +{/* */} + + + + +{/* */} + + + + +{/* */} + + + +[Learn more about using WebAuthn passkeys in your application code](/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/#webauthn-passkeys). + +### Managing credentials + +{/* quick blurb then segue over to "manage WebAuthn credentials" page */} + +[Learn more about managing WebAuthn credentials](/[platform]/build-a-backend/auth/manage-users/manage-webauthn-credentials). diff --git a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/multi-step-sign-in/index.mdx b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/multi-step-sign-in/index.mdx index eee41bf9520..97c53c2d0b6 100644 --- a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/multi-step-sign-in/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/multi-step-sign-in/index.mdx @@ -80,6 +80,21 @@ if (nextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP') { }); } +if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_PASSWORD') { + // collect password from user + await confirmSignIn({ + challengeResponse: 'hunter2', + }); +} + +if (nextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION') { + // present nextStep.availableChallenges to user + // collect user selection + await confirmSignIn({ + challengeResponse: 'SMS_OTP', // or 'EMAIL_OTP', 'WEB_AUTHN', 'PASSWORD', 'PASSWORD_SRP' + }); +} + if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE') { // collect custom challenge answer from user await confirmSignIn({ @@ -361,6 +376,78 @@ async function handleMfaSelection(mfaType: MfaType) { ``` +## Confirm sign-in with Password + +If the next step is `CONFIRM_SIGN_IN_WITH_PASSWORD`, the user must provide their password as the first factor authentication method. To handle this step, your implementation should prompt the user to enter their password. After the user enters the password, pass the value to the `confirmSignIn` API. + +```ts +import { type SignInOutput, confirmSignIn } from '@aws-amplify/auth'; + +async function handleSignInResult(result: SignInOutput) { + switch (result.nextStep.signInStep) { + case 'CONFIRM_SIGN_IN_WITH_PASSWORD': { + // Prompt user to enter their password + console.log(`Please enter your password.`); + break; + } + } +} + +async function confirmWithPassword(password: string) { + const result = await confirmSignIn({ challengeResponse: password }); + + return handleSignInResult(result); +} +``` + +## Continue sign-in with First Factor Selection + +If the next step is `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION`, the user must select a first factor method for authentication. After the user selects an option, your implementation should pass the selected method to the `confirmSignIn` API. + +The first factor types which are currently supported by Amplify Auth are: +- `SMS_OTP` +- `EMAIL_OTP` +- `WEB_AUTHN` +- `PASSWORD` +- `PASSWORD_SRP` + +Depending on your configuration and what factors the user has previously setup, not all options may be available. Only the available options will be presented in `availableChallenges` for selection. + +Once Amplify receives the user's selection via the `confirmSignIn` API, you can expect to handle a follow up `nextStep` corresponding with the first factor type selected: +- If `SMS_OTP` is selected, `CONFIRM_SIGN_IN_WITH_SMS_CODE` will be the next step. +- If `EMAIL_OTP` is selected, `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` will be the next step. +- If `PASSWORD` or `PASSWORD_SRP` is selected, `CONFIRM_SIGN_IN_WITH_PASSWORD` will be the next step. +- If `WEB_AUTHN` is selected, Amplify Auth will initiate the authentication ceremony on the user's device. If successful, the next step will be `DONE`. + + +```ts +import { type SignInOutput, confirmSignIn } from '@aws-amplify/auth'; + +async function handleSignInResult(result: SignInOutput) { + switch (result.nextStep.signInStep) { + case 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION': { + const { availableChallenges } = result.nextStep; + // Present available first factor options to user + // Prompt for selection + console.log( + `There are multiple first factor options available for sign in.`, + ); + console.log( + `Select a first factor type from the availableChallenges list.`, + ); + break; + } + } +} + +async function handleFirstFactorSelection(firstFactorType: string) { + const result = await confirmSignIn({ challengeResponse: firstFactorType }); + + return handleSignInResult(result); +} + +``` + ## Confirm sign-in with custom challenge If the next step is `CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE`, Amplify Auth is awaiting completion of a custom authentication challenge. The challenge is based on the AWS Lambda trigger you configured as part of a custom sign in flow. @@ -1067,102 +1154,111 @@ The `nextStep` property is of enum type `AuthSignInStep`. Depending on its value ```java try { - AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.USER_SRP_AUTH).build(); - Amplify.Auth.signIn( - "username", - "password", - options, - result -> - { - AuthNextSignInStep nextStep = result.getNextStep(); - switch (nextStep.getSignInStep()) { - case CONFIRM_SIGN_IN_WITH_TOTP_CODE: { - Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code"); - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - break; - } - case CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION: { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); - Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); - // Prompt the user to select the MFA type they want to setup - // Then invoke `confirmSignIn` api with the MFA type - break; - } - case CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP: { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); - // Prompt the user to enter the email address they would like to use to receive OTPs - // Then invoke `confirmSignIn` api with the email address - break; - } - case CONTINUE_SIGN_IN_WITH_TOTP_SETUP: { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP"); - Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app" + nextStep.getTotpSetupDetails().getSharedSecret()); - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - break; - } - case CONTINUE_SIGN_IN_WITH_MFA_SELECTION: { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type"); - Log.i("AuthQuickstart", "Allowed MFA type" + nextStep.getAllowedMFATypes()); - // Prompt the user to select the MFA type they want to use - // Then invoke `confirmSignIn` api with the MFA type - break; - } - case CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE: { - Log.i("AuthQuickstart", "SMS code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); - Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); - // Prompt the user to enter the SMS MFA code they received - // Then invoke `confirmSignIn` api with the code - break; - } - case CONFIRM_SIGN_IN_WITH_OTP: { - Log.i("AuthQuickstart", "OTP code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); - Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); - // Prompt the user to enter the OTP MFA code they received - // Then invoke `confirmSignIn` api with the code - break; - } - case CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE: { - Log.i("AuthQuickstart", "Custom challenge, additional info: " + nextStep.getAdditionalInfo()); - // Prompt the user to enter custom challenge answer - // Then invoke `confirmSignIn` api with the answer - break; - } - case CONFIRM_SIGN_IN_WITH_NEW_PASSWORD: { - Log.i("AuthQuickstart", "Sign in with new password, additional info: " + nextStep.getAdditionalInfo()); - // Prompt the user to enter a new password - // Then invoke `confirmSignIn` api with new password - break; - } - case DONE: { - Log.i("AuthQuickstart", "SignIn complete"); - // User has successfully signed in to the app - break; - } - } - }, - error -> { - if (error instanceof UserNotConfirmedException) { - // User was not confirmed during the signup process. - // Invoke `confirmSignUp` api to confirm the user if - // they have the confirmation code. If they do not have the - // confirmation code, invoke `resendSignUpCode` to send the - // code again. - // After the user is confirmed, invoke the `signIn` api again. - Log.i("AuthQuickstart", "Signup confirmation required" + error); - } else if (error instanceof PasswordResetRequiredException) { - // User needs to reset their password. - // Invoke `resetPassword` api to start the reset password - // flow, and once reset password flow completes, invoke - // `signIn` api to trigger signIn flow again. - Log.i("AuthQuickstart", "Password reset required" + error); - } else { - Log.e("AuthQuickstart", "SignIn failed: " + error); - } - } - - ); + Amplify.Auth.signIn( + "hello@example.com", + "password", + result -> + { + AuthNextSignInStep nextStep = result.getNextStep(); + switch (nextStep.getSignInStep()) { + case CONFIRM_SIGN_IN_WITH_TOTP_CODE: { + Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code"); + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + break; + } + case CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION: { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); + Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); + // Prompt the user to select the MFA type they want to setup + // Then invoke `confirmSignIn` api with the MFA type + break; + } + case CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP: { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); + // Prompt the user to enter the email address they would like to use to receive OTPs + // Then invoke `confirmSignIn` api with the email address + break; + } + case CONTINUE_SIGN_IN_WITH_TOTP_SETUP: { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP"); + Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app" + nextStep.getTotpSetupDetails().getSharedSecret()); + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + break; + } + case CONTINUE_SIGN_IN_WITH_MFA_SELECTION: { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type"); + Log.i("AuthQuickstart", "Allowed MFA type" + nextStep.getAllowedMFATypes()); + // Prompt the user to select the MFA type they want to use + // Then invoke `confirmSignIn` api with the MFA type + break; + } + case CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION: { + Log.i("AuthQuickstart", "Available authentication factors for this user: " + result.getNextStep().getAvailableFactors()); + // Prompt the user to select which authentication factor they want to use to sign-in + // Then invoke `confirmSignIn` api with that selection + break; + } + case CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE: { + Log.i("AuthQuickstart", "SMS code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); + Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); + // Prompt the user to enter the SMS MFA code they received + // Then invoke `confirmSignIn` api with the code + break; + } + case CONFIRM_SIGN_IN_WITH_OTP: { + Log.i("AuthQuickstart", "OTP code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); + Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); + // Prompt the user to enter the OTP MFA code they received + // Then invoke `confirmSignIn` api with the code + break; + } + case CONFIRM_SIGN_IN_WITH_PASSWORD: { + Log.i("AuthQuickstart", "Received next step as confirm sign in with password"); + // Prompt the user to enter their password + // Then invoke `confirmSignIn` api with that password + break; + } + case CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE: { + Log.i("AuthQuickstart", "Custom challenge, additional info: " + nextStep.getAdditionalInfo()); + // Prompt the user to enter custom challenge answer + // Then invoke `confirmSignIn` api with the answer + break; + } + case CONFIRM_SIGN_IN_WITH_NEW_PASSWORD: { + Log.i("AuthQuickstart", "Sign in with new password, additional info: " + nextStep.getAdditionalInfo()); + // Prompt the user to enter a new password + // Then invoke `confirmSignIn` api with new password + break; + } + case DONE: { + Log.i("AuthQuickstart", "SignIn complete"); + // User has successfully signed in to the app + break; + } + } + }, + error -> { + if (error instanceof UserNotConfirmedException) { + // User was not confirmed during the signup process. + // Invoke `confirmSignUp` api to confirm the user if + // they have the confirmation code. If they do not have the + // confirmation code, invoke `resendSignUpCode` to send the + // code again. + // After the user is confirmed, invoke the `signIn` api again. + Log.i("AuthQuickstart", "Signup confirmation required" + error); + } else if (error instanceof PasswordResetRequiredException) { + // User needs to reset their password. + // Invoke `resetPassword` api to start the reset password + // flow, and once reset password flow completes, invoke + // `signIn` api to trigger signIn flow again. + Log.i("AuthQuickstart", "Password reset required" + error); + } else { + Log.e("AuthQuickstart", "SignIn failed: " + error); + } + } + ); } catch (Exception error) { Log.e("AuthQuickstart", "Unexpected error occurred: " + error); } @@ -1173,91 +1269,103 @@ try { ```kotlin -val options = AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.USER_SRP_AUTH).build() try { - Amplify.Auth.signIn( - "username", - "password", - options, - { result -> - val nextStep = result.nextStep - when(nextStep.signInStep){ - AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE -> { - Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code") - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - } - AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); - Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); - // Prompt the user to select the MFA type they want to setup - // Then invoke `confirmSignIn` api with the MFA type - } - AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); - // Prompt the user to enter the email address they would like to use to receive OTPs - // Then invoke `confirmSignIn` api with the email address - } - AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP") - Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app ${nextStep.totpSetupDetails.sharedSecret}") - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - } - AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type") - Log.i("AuthQuickstart", "Allowed MFA types ${nextStep.allowedMFATypes}") - // Prompt the user to select the MFA type they want to use - // Then invoke `confirmSignIn` api with the MFA type - } - AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE -> { - Log.i("AuthQuickstart", "SMS code sent to ${nextStep.codeDeliveryDetails?.destination}") - Log.i("AuthQuickstart", "Additional Info ${nextStep.additionalInfo}") - // Prompt the user to enter the SMS MFA code they received - // Then invoke `confirmSignIn` api with the code - } - AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP -> { - Log.i("AuthQuickstart", "OTP code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); - Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); - // Prompt the user to enter the OTP MFA code they received - // Then invoke `confirmSignIn` api with the code - } - AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE -> { - Log.i("AuthQuickstart","Custom challenge, additional info: ${nextStep.additionalInfo}") - // Prompt the user to enter custom challenge answer - // Then invoke `confirmSignIn` api with the answer - } - AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD -> { - Log.i("AuthQuickstart", "Sign in with new password, additional info: ${nextStep.additionalInfo}") - // Prompt the user to enter a new password - // Then invoke `confirmSignIn` api with new password - } - AuthSignInStep.DONE -> { - Log.i("AuthQuickstart", "SignIn complete") - // User has successfully signed in to the app - } - } + Amplify.Auth.signIn( + "hello@example.com", + "password", + { result -> + val nextStep = result.nextStep + when(nextStep.signInStep){ + AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE -> { + Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code") + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION -> { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup") + Log.i("AuthQuickstart", "Allowed MFA types for setup ${nextStep.allowedMFATypes}") + // Prompt the user to select the MFA type they want to setup + // Then invoke `confirmSignIn` api with the MFA type + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP -> { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA") + // Prompt the user to enter the email address they would like to use to receive OTPs + // Then invoke `confirmSignIn` api with the email address + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP -> { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP") + Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app ${nextStep.totpSetupDetails?.sharedSecret}") + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION -> { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type") + Log.i("AuthQuickstart", "Allowed MFA types ${nextStep.allowedMFATypes}") + // Prompt the user to select the MFA type they want to use + // Then invoke `confirmSignIn` api with the MFA type + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION -> { + Log.i("AuthQuickstart", "Available authentication factors for this user: ${result.nextStep.availableFactors}") + // Prompt the user to select which authentication factor they want to use to sign-in + // Then invoke `confirmSignIn` api with that selection + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE -> { + Log.i("AuthQuickstart", "SMS code sent to ${nextStep.codeDeliveryDetails?.destination}") + Log.i("AuthQuickstart", "Additional Info ${nextStep.additionalInfo}") + // Prompt the user to enter the SMS MFA code they received + // Then invoke `confirmSignIn` api with the code + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP -> { + Log.i("AuthQuickstart", "OTP code sent to ${nextStep.codeDeliveryDetails?.destination}") + Log.i("AuthQuickstart", "Additional Info ${nextStep.additionalInfo}") + // Prompt the user to enter the OTP MFA code they received + // Then invoke `confirmSignIn` api with the code + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD -> { + Log.i("AuthQuickstart", "Received next step as confirm sign in with password") + // Prompt the user to enter their password + // Then invoke `confirmSignIn` api with that password + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE -> { + Log.i("AuthQuickstart","Custom challenge, additional info: ${nextStep.additionalInfo}") + // Prompt the user to enter custom challenge answer + // Then invoke `confirmSignIn` api with the answer + } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD -> { + Log.i("AuthQuickstart", "Sign in with new password, additional info: ${nextStep.additionalInfo}") + // Prompt the user to enter a new password + // Then invoke `confirmSignIn` api with new password + } + AuthSignInStep.DONE -> { + Log.i("AuthQuickstart", "SignIn complete") + // User has successfully signed in to the app + } + } - } - ) { error -> - if (error is UserNotConfirmedException) { - // User was not confirmed during the signup process. - // Invoke `confirmSignUp` api to confirm the user if - // they have the confirmation code. If they do not have the - // confirmation code, invoke `resendSignUpCode` to send the - // code again. - // After the user is confirmed, invoke the `signIn` api again. - Log.i("AuthQuickstart", "Signup confirmation required", error) - } else if (error is PasswordResetRequiredException) { - // User needs to reset their password. - // Invoke `resetPassword` api to start the reset password - // flow, and once reset password flow completes, invoke - // `signIn` api to trigger signIn flow again. - Log.i("AuthQuickstart", "Password reset required", error) - } else { - Log.e("AuthQuickstart", "Unexpected error occurred: $error") - } - } + } + ) { error -> + when (error) { + is UserNotConfirmedException -> { + // User was not confirmed during the signup process. + // Invoke `confirmSignUp` api to confirm the user if + // they have the confirmation code. If they do not have the + // confirmation code, invoke `resendSignUpCode` to send the + // code again. + // After the user is confirmed, invoke the `signIn` api again. + Log.e("AuthQuickstart", "Signup confirmation required", error) + } + is PasswordResetRequiredException -> { + // User needs to reset their password. + // Invoke `resetPassword` api to start the reset password + // flow, and once reset password flow completes, invoke + // `signIn` api to trigger signIn flow again. + Log.e("AuthQuickstart", "Password reset required", error) + } + else -> { + Log.e("AuthQuickstart", "Unexpected error occurred: $error") + } + } + } } catch (error: Exception) { Log.e("AuthQuickstart", "Unexpected error occurred: $error") } @@ -1268,13 +1376,10 @@ try { ```kotlin -val options = - AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.USER_SRP_AUTH).build() try { val result = Amplify.Auth.signIn( - "username", - "password", - options + "hello@example.com", + "password" ) val nextStep = result.nextStep when (nextStep.signInStep) { @@ -1284,19 +1389,19 @@ try { // Then invoke `confirmSignIn` api with the code } AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); - Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup") + Log.i("AuthQuickstart", "Allowed MFA types for setup ${nextStep.allowedMFATypes}") // Prompt the user to select the MFA type they want to setup // Then invoke `confirmSignIn` api with the MFA type } AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP -> { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA") // Prompt the user to enter the email address they would like to use to receive OTPs // Then invoke `confirmSignIn` api with the email address } AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP -> { Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP") - Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app ${nextStep.totpSetupDetails.sharedSecret}") + Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app ${nextStep.totpSetupDetails?.sharedSecret}") // Prompt the user to enter the TOTP code generated in their authenticator app // Then invoke `confirmSignIn` api with the code } @@ -1306,6 +1411,11 @@ try { // Prompt the user to select the MFA type they want to use // Then invoke `confirmSignIn` api with the MFA type } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION -> { + Log.i("AuthQuickstart", "Available authentication factors for this user: ${result.nextStep.availableFactors}") + // Prompt the user to select which authentication factor they want to use to sign-in + // Then invoke `confirmSignIn` api with that selection + } AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE -> { Log.i("AuthQuickstart", "SMS code sent to ${nextStep.codeDeliveryDetails?.destination}") Log.i("AuthQuickstart", "Additional Info ${nextStep.additionalInfo}") @@ -1318,16 +1428,18 @@ try { // Prompt the user to enter the OTP MFA code they received // Then invoke `confirmSignIn` api with the code } + AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD -> { + Log.i("AuthQuickstart", "Received next step as confirm sign in with password") + // Prompt the user to enter their password + // Then invoke `confirmSignIn` api with that password + } AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE -> { - Log.i("AuthQuickstart", "Custom challenge, additional info: ${nextStep.additionalInfo}") + Log.i("AuthQuickstart","Custom challenge, additional info: ${nextStep.additionalInfo}") // Prompt the user to enter custom challenge answer // Then invoke `confirmSignIn` api with the answer } AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD -> { - Log.i( - "AuthQuickstart", - "Sign in with new password, additional info: ${nextStep.additionalInfo}" - ) + Log.i("AuthQuickstart", "Sign in with new password, additional info: ${nextStep.additionalInfo}") // Prompt the user to enter a new password // Then invoke `confirmSignIn` api with new password } @@ -1337,23 +1449,27 @@ try { } } } catch (error: Exception) { - if (error is UserNotConfirmedException) { - // User was not confirmed during the signup process. - // Invoke `confirmSignUp` api to confirm the user if - // they have the confirmation code. If they do not have the - // confirmation code, invoke `resendSignUpCode` to send the - // code again. - // After the user is confirmed, invoke the `signIn` api again. - Log.i("AuthQuickstart", "Signup confirmation required", error) - } else if (error is PasswordResetRequiredException) { - // User needs to reset their password. - // Invoke `resetPassword` api to start the reset password - // flow, and once reset password flow completes, invoke - // `signIn` api to trigger signIn flow again. - Log.i("AuthQuickstart", "Password reset required", error) - } else { - Log.e("AuthQuickstart", "Unexpected error occurred: $error") - } + when (error) { + is UserNotConfirmedException -> { + // User was not confirmed during the signup process. + // Invoke `confirmSignUp` api to confirm the user if + // they have the confirmation code. If they do not have the + // confirmation code, invoke `resendSignUpCode` to send the + // code again. + // After the user is confirmed, invoke the `signIn` api again. + Log.e("AuthQuickstart", "Signup confirmation required", error) + } + is PasswordResetRequiredException -> { + // User needs to reset their password. + // Invoke `resetPassword` api to start the reset password + // flow, and once reset password flow completes, invoke + // `signIn` api to trigger signIn flow again. + Log.e("AuthQuickstart", "Password reset required", error) + } + else -> { + Log.e("AuthQuickstart", "Unexpected error occurred: $error") + } + } } ``` @@ -1362,98 +1478,108 @@ try { ```java - -AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder().authFlowType(AuthFlowType.USER_SRP_AUTH).build(); -RxAmplify.Auth.signIn("username", "password", options).subscribe( - result -> - { - AuthNextSignInStep nextStep = result.getNextStep(); - switch (nextStep.getSignInStep()) { - case CONFIRM_SIGN_IN_WITH_TOTP_CODE: { - Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code"); - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - break; - } - case CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION: { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); - Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); - // Prompt the user to select the MFA type they want to setup - // Then invoke `confirmSignIn` api with the MFA type - break; - } - case CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP: { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); - // Prompt the user to enter the email address they would like to use to receive OTPs - // Then invoke `confirmSignIn` api with the email address - break; - } - case CONTINUE_SIGN_IN_WITH_TOTP_SETUP: { - Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP"); - Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app" + nextStep.getTotpSetupDetails().getSharedSecret()); - // Prompt the user to enter the TOTP code generated in their authenticator app - // Then invoke `confirmSignIn` api with the code - break; - } - case CONTINUE_SIGN_IN_WITH_MFA_SELECTION: { - Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type"); - Log.i("AuthQuickstart", "Allowed MFA type" + nextStep.getAllowedMFATypes()); - // Prompt the user to select the MFA type they want to use - // Then invoke `confirmSignIn` api with the MFA type - break; - } - case CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE: { - Log.i("AuthQuickstart", "SMS code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); - Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); - // Prompt the user to enter the SMS MFA code they received - // Then invoke `confirmSignIn` api with the code - break; - } - case CONFIRM_SIGN_IN_WITH_OTP: { - Log.i("AuthQuickstart", "OTP code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); - Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); - // Prompt the user to enter the OTP MFA code they received - // Then invoke `confirmSignIn` api with the code - break; - } - case CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE: { - Log.i("AuthQuickstart", "Custom challenge, additional info: " + nextStep.getAdditionalInfo()); - // Prompt the user to enter custom challenge answer - // Then invoke `confirmSignIn` api with the answer - break; - } - case CONFIRM_SIGN_IN_WITH_NEW_PASSWORD: { - Log.i("AuthQuickstart", "Sign in with new password, additional info: " + nextStep.getAdditionalInfo()); - // Prompt the user to enter a new password - // Then invoke `confirmSignIn` api with new password - break; - } - case DONE: { - Log.i("AuthQuickstart", "SignIn complete"); - // User has successfully signed in to the app - break; - } +RxAmplify.Auth.signIn("hello@example.com", "password").subscribe( + result -> + { + AuthNextSignInStep nextStep = result.getNextStep(); + switch (nextStep.getSignInStep()) { + case CONFIRM_SIGN_IN_WITH_TOTP_CODE: { + Log.i("AuthQuickstart", "Received next step as confirm sign in with TOTP code"); + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + break; } - }, - error -> { - if (error instanceof UserNotConfirmedException) { - // User was not confirmed during the signup process. - // Invoke `confirmSignUp` api to confirm the user if - // they have the confirmation code. If they do not have the - // confirmation code, invoke `resendSignUpCode` to send the - // code again. - // After the user is confirmed, invoke the `signIn` api again. - Log.i("AuthQuickstart", "Signup confirmation required" + error); - } else if (error instanceof PasswordResetRequiredException) { - // User needs to reset their password. - // Invoke `resetPassword` api to start the reset password - // flow, and once reset password flow completes, invoke - // `signIn` api to trigger signIn flow again. - Log.i("AuthQuickstart", "Password reset required" + error); - } else { - Log.e("AuthQuickstart", "SignIn failed: " + error); - } + case CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION: { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting an MFA method to setup"); + Log.i("AuthQuickstart", "Allowed MFA types for setup" + nextStep.getAllowedMFATypes()); + // Prompt the user to select the MFA type they want to setup + // Then invoke `confirmSignIn` api with the MFA type + break; + } + case CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP: { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up email MFA"); + // Prompt the user to enter the email address they would like to use to receive OTPs + // Then invoke `confirmSignIn` api with the email address + break; + } + case CONTINUE_SIGN_IN_WITH_TOTP_SETUP: { + Log.i("AuthQuickstart", "Received next step as continue sign in by setting up TOTP"); + Log.i("AuthQuickstart", "Shared secret that will be used to set up TOTP in the authenticator app" + nextStep.getTotpSetupDetails().getSharedSecret()); + // Prompt the user to enter the TOTP code generated in their authenticator app + // Then invoke `confirmSignIn` api with the code + break; + } + case CONTINUE_SIGN_IN_WITH_MFA_SELECTION: { + Log.i("AuthQuickstart", "Received next step as continue sign in by selecting MFA type"); + Log.i("AuthQuickstart", "Allowed MFA type" + nextStep.getAllowedMFATypes()); + // Prompt the user to select the MFA type they want to use + // Then invoke `confirmSignIn` api with the MFA type + break; + } + case CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION: { + Log.i("AuthQuickstart", "Available authentication factors for this user: " + result.getNextStep().getAvailableFactors()); + // Prompt the user to select which authentication factor they want to use to sign-in + // Then invoke `confirmSignIn` api with that selection + break; + } + case CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE: { + Log.i("AuthQuickstart", "SMS code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); + Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); + // Prompt the user to enter the SMS MFA code they received + // Then invoke `confirmSignIn` api with the code + break; + } + case CONFIRM_SIGN_IN_WITH_OTP: { + Log.i("AuthQuickstart", "OTP code sent to " + nextStep.getCodeDeliveryDetails().getDestination()); + Log.i("AuthQuickstart", "Additional Info :" + nextStep.getAdditionalInfo()); + // Prompt the user to enter the OTP MFA code they received + // Then invoke `confirmSignIn` api with the code + break; + } + case CONFIRM_SIGN_IN_WITH_PASSWORD: { + Log.i("AuthQuickstart", "Received next step as confirm sign in with password"); + // Prompt the user to enter their password + // Then invoke `confirmSignIn` api with that password + break; + } + case CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE: { + Log.i("AuthQuickstart", "Custom challenge, additional info: " + nextStep.getAdditionalInfo()); + // Prompt the user to enter custom challenge answer + // Then invoke `confirmSignIn` api with the answer + break; + } + case CONFIRM_SIGN_IN_WITH_NEW_PASSWORD: { + Log.i("AuthQuickstart", "Sign in with new password, additional info: " + nextStep.getAdditionalInfo()); + // Prompt the user to enter a new password + // Then invoke `confirmSignIn` api with new password + break; + } + case DONE: { + Log.i("AuthQuickstart", "SignIn complete"); + // User has successfully signed in to the app + break; + } + } + }, + error -> { + if (error instanceof UserNotConfirmedException) { + // User was not confirmed during the signup process. + // Invoke `confirmSignUp` api to confirm the user if + // they have the confirmation code. If they do not have the + // confirmation code, invoke `resendSignUpCode` to send the + // code again. + // After the user is confirmed, invoke the `signIn` api again. + Log.i("AuthQuickstart", "Signup confirmation required" + error); + } else if (error instanceof PasswordResetRequiredException) { + // User needs to reset their password. + // Invoke `resetPassword` api to start the reset password + // flow, and once reset password flow completes, invoke + // `signIn` api to trigger signIn flow again. + Log.i("AuthQuickstart", "Password reset required" + error); + } else { + Log.e("AuthQuickstart", "SignIn failed: " + error); } + } ); ``` @@ -1465,7 +1591,11 @@ RxAmplify.Auth.signIn("username", "password", options).subscribe( If the next step is `CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE`, Amplify Auth has sent the user a random code over SMS, and is waiting to find out if the user successfully received it. To handle this step, your app's UI must prompt the user to enter the code. After the user enters the code, your implementation must pass the value to Amplify Auth `confirmSignIn` API. -Note: the signIn result also includes an `AuthCodeDeliveryDetails` member. It includes additional information about the code delivery such as the partial phone number of the SMS recipient. + + +**Note:** The result also includes an `AuthCodeDeliveryDetails` member. It includes additional information about the code delivery such as the partial phone number of the SMS recipient. + + @@ -1572,7 +1702,21 @@ After the user enters the code, your implementation must pass the value to Ampli If the next step is `CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE`, Amplify Auth has sent the user a random code to their email address and is waiting to find out if the user successfully received it. To handle this step, your app's UI must prompt the user to enter the code. After the user enters the code, your implementation must pass the value to Amplify Auth `confirmSignIn` API. -Note: the signIn result also includes an `AuthCodeDeliveryDetails` member. It includes additional information about the code delivery such as the partial email address of the recipient. + + +**Note:** The result also includes an `AuthCodeDeliveryDetails` member. It includes additional information about the code delivery such as the partial email address of the recipient. + + + +## Confirm sign-in with OTP + +If the next step is `CONFIRM_SIGN_IN_WITH_OTP`, Amplify Auth has sent the user a random code to the medium of the user's choosing (e.g. SMS or email) and is waiting for the user to verify that code. To handle this step, your app's UI must prompt the user to enter the code. After the user enters the code, pass the value to the `confirmSignIn` API. + + + +**Note:** The result includes an `AuthCodeDeliveryDetails` member. It includes additional information about the code delivery, such as the partial email address of the recipient, which can be used to prompt the user on where to look for the code. + + ## Continue sign-in with MFA Selection @@ -1592,6 +1736,12 @@ Once the authenticator app is set up, the user can generate a TOTP code and prov If the next step is `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION`, the user must select the MFA method to setup. Amplify Auth currently supports SMS, TOTP, and email as MFA methods. After the user selects an MFA method, your implementation must pass the selected MFA method to Amplify Auth using `confirmSignIn` API. +## Continue sign-in with First Factor Selection + +If the next step is `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION`, the user must select an authentication factor to use either because they did not specify one or because the one they chose is not supported (e.g. selecting SMS when they don't have a phone number registered to their account). Amplify Auth currently supports SMS, email, password, and webauthn as authentication factors. After the user selects an authentication method, your implementation must pass the selected authentication method to Amplify Auth using `confirmSignIn` API. + +Visit the [sign-in documentation](/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/#sign-in-with-passwordless-methods) to see examples on how to call the `confirmSignIn` API. + ## Confirm sign-in with custom challenge If the next step is `CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE`, Amplify Auth is awaiting completion of a custom authentication challenge. The challenge is based on the Lambda trigger you setup when you configured a [custom sign in flow](/[platform]/build-a-backend/auth/customize-auth-lifecycle/custom-auth-flows/#sign-in-a-user). To complete this step, you should prompt the user for the custom challenge answer, and pass the answer to the `confirmSignIn` API. @@ -1933,7 +2083,10 @@ RxAmplify.Auth.confirmSignUp( This call fetches the current logged in user and should be used after a user has been successfully signed in. If the user is signed in, it will return the current userId and username. -Note: An empty string will be assigned to userId and/or username, if the values are not present in the accessToken. + + +**Note:** An empty string will be assigned to userId and/or username, if the values are not present in the accessToken. + @@ -2030,6 +2183,19 @@ func signIn(username: String, password: String) async { // Prompt the user to enter the Email MFA code they received // Then invoke `confirmSignIn` api with the code + + case .continueSignInWithFirstFactorSelection(let allowedFactors): + print("Received next step as continue sign in by selecting first factor") + print("Allowed factors \(allowedFactors)") + + // Prompt the user to select the first factor they want to use + // Then invoke `confirmSignIn` api with the factor + + case .confirmSignInWithPassword: + print("Received next step as confirm sign in with password") + + // Prompt the user to enter the password + // Then invoke `confirmSignIn` api with the password case .continueSignInWithTOTPSetup(let setUpDetails): print("Received next step as continue sign in by setting up TOTP") diff --git a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/index.mdx b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/index.mdx index d018ea92424..d8efe532254 100644 --- a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/index.mdx @@ -209,7 +209,7 @@ RxAmplify.Auth.signIn("username", "password") func signIn(username: String, password: String) async { do { let signInResult = try await Amplify.Auth.signIn( - username: username, + username: username, password: password ) if signInResult.isSignedIn { @@ -230,7 +230,7 @@ func signIn(username: String, password: String) async { func signIn(username: String, password: String) -> AnyCancellable { Amplify.Publisher.create { try await Amplify.Auth.signIn( - username: username, + username: username, password: password ) }.sink { @@ -259,8 +259,10 @@ The `signIn` API response will include a `nextStep` property, which can be used | `CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED` | The user was created with a temporary password and must set a new one. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE` | The sign-in must be confirmed with a custom challenge response. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | -| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | -| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with a EMAIL code from the user. Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with an SMS code from the user. Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with an EMAIL code from the user. Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. | +| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | The user must select their mode of first factor authentication. Complete the process by passing the desired mode to the `challengeResponse` field of `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `"EMAIL"` or `"TOTP"` to `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | @@ -278,10 +280,12 @@ The `signIn` API response will include a `nextStep` property, which can be used | `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_OTP` | The sign-in must be confirmed with a code from the user (sent via SMS or Email). Complete the process with `confirmSignIn`. | -| `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `MFAType.EMAIL.challengeResponse` or `MFAType.TOTP.challengeResponse` to `confirmSignIn`. | +| `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP` | The EMAIL setup process must be continued. Complete the process by passing a valid email address to `confirmSignIn`. | +| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | The user must select their mode of first factor authentication. Complete the process by passing the desired mode to the `challengeResponse` field of `confirmSignIn`. | | `RESET_PASSWORD` | The user must reset their password via `resetPassword`. | | `CONFIRM_SIGN_UP` | The user hasn't completed the sign-up flow fully and must be confirmed via `confirmSignUp`. | | `DONE` | The sign in process has been completed. | @@ -295,6 +299,8 @@ The `signIn` API response will include a `nextStep` property, which can be used | `confirmSignInWithTOTPCode` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | | `confirmSignInWithSMSMFACode` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | | `confirmSignInWithOTP` | The sign-in must be confirmed with a code from the user (sent via SMS or Email). Complete the process with `confirmSignIn`. | +| `confirmSignInWithPassword` | The user must set a new password. Complete the process with `confirmSignIn`. | +| `continueSignInWithFirstFactorSelection` | The user must select their preferred mode of First Factor authentication. Complete the process with `confirmSignIn`. | | `continueSignInWithMFASelection` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `continueSignInWithMFASetupSelection` | The user must select their mode of MFA verification to setup. Complete the process by passing either `MFAType.email.challengeResponse` or `MFAType.totp.challengeResponse ` to `confirmSignIn`. | | `continueSignInWithTOTPSetup` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | @@ -589,6 +595,8 @@ Following sign in, you will receive a `nextStep` in the sign-in result of one of | `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_SMS_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_EMAIL_CODE` | The sign-in must be confirmed with a EMAIL code from the user. Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. | +| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | The user must select their mode of first factor authentication. Complete the process by passing the desired mode to the `challengeResponse` field of `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `"EMAIL"` or `"TOTP"` to `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | @@ -602,6 +610,8 @@ Following sign in, you will receive a `nextStep` in the sign-in result of one of | `CONFIRM_SIGN_IN_WITH_TOTP_CODE` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | | `CONFIRM_SIGN_IN_WITH_OTP` | The sign-in must be confirmed with a code from the user (sent via SMS or Email). Complete the process with `confirmSignIn`. | +| `CONFIRM_SIGN_IN_WITH_PASSWORD` | The sign-in must be confirmed with the password from the user. Complete the process with `confirmSignIn`. | +| `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` | The user must select their mode of first factor authentication. Complete the process by passing the desired mode to the `challengeResponse` field of `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SELECTION` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION` | The user must select their mode of MFA verification to setup. Complete the process by passing either `MFAType.EMAIL.challengeResponse` or `MFAType.TOTP.challengeResponse` to `confirmSignIn`. | | `CONTINUE_SIGN_IN_WITH_TOTP_SETUP` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | @@ -615,6 +625,8 @@ Following sign in, you will receive a `nextStep` in the sign-in result of one of | `confirmSignInWithTOTPCode` | The sign-in must be confirmed with a TOTP code from the user. Complete the process with `confirmSignIn`. | | `confirmSignInWithSMSMFACode` | The sign-in must be confirmed with a SMS code from the user. Complete the process with `confirmSignIn`. | | `confirmSignInWithOTP` | The sign-in must be confirmed with a code from the user (sent via SMS or Email). Complete the process with `confirmSignIn`. | +| `confirmSignInWithPassword` | The user must set a new password. Complete the process with `confirmSignIn`. | +| `continueSignInWithFirstFactorSelection` | The user must select their preferred mode of First Factor authentication. Complete the process with `confirmSignIn`. | | `continueSignInWithMFASelection` | The user must select their mode of MFA verification before signing in. Complete the process with `confirmSignIn`. | | `continueSignInWithMFASetupSelection` | The user must select their mode of MFA verification to setup. Complete the process by passing either `MFAType.email.challengeResponse` or `MFAType.totp.challengeResponse ` to `confirmSignIn`. | | `continueSignInWithTOTPSetup` | The TOTP setup process must be continued. Complete the process with `confirmSignIn`. | @@ -695,6 +707,11 @@ if (nextStep.signInStep === "CONTINUE_SIGN_IN_WITH_TOTP_SETUP") { } ``` + + +**Note:** The Amplify authentication flow will persist relevant session data throughout the lifespan of a page session. This enables the `confirmSignIn` API to be leveraged even after a full page refresh in a multi-page application, such as when redirecting from a login page to a sign in confirmation page. + + @@ -727,7 +744,7 @@ Amplify.Auth.confirmSignIn("code received via SMS", ```kotlin try { val result = Amplify.Auth.confirmSignIn("code received via SMS") - Log.i("AuthQuickstart", "Confirmed signin: $result") + Log.i("AuthQuickstart", "Confirmed signin: $result") } catch (error: AuthException) { Log.e("AuthQuickstart", "Failed to confirm signin", error) } @@ -791,9 +808,15 @@ func confirmSignIn() -> AnyCancellable { ## Sign in with an external identity provider + + To sign in using an external identity provider such as Google, use the `signInWithRedirect` function. - + + +For guidance on configuring an external Identity Provider with Amplify see [External Identity Providers](/[platform]/build-a-backend/auth/concepts/external-identity-providers/) + + ```ts import { signInWithRedirect } from "aws-amplify/auth" @@ -818,7 +841,7 @@ signInWithRedirect({ provider: { }}) ``` -### Auto sign-in +## Auto sign-in The `autoSignIn` API will automatically sign-in a user when it was previously enabled by the `signUp` API and after any of the following cases has completed: @@ -830,9 +853,14 @@ import { autoSignIn } from 'aws-amplify/auth'; await autoSignIn(); ``` + +**Note**: When MFA is enabled, your users may be presented with multiple consecutive steps that require them to enter an OTP to proceed with the sign up and subsequent sign in flow. This requirement is not present when using the `USER_AUTH` flow. + - + + ### Install native module @@ -871,6 +899,8 @@ Add the `intent-filter` to your application's main activity, replacing `myapp` w +To sign in using an external identity provider such as Google, use the `signInWithWebUI` function. + ### How It Works Sign-in with web UI will display the sign-in UI inside a webview. After the sign-in process is complete, the sign-in UI will redirect back to your app. @@ -937,7 +967,10 @@ Future socialSignIn() async { ``` -## Update AndroidManifest.xml + +To sign in using an external identity provider such as Google, use the `signInWithSocialWebUI` function. + +### Update AndroidManifest.xml Add the following activity and queries tag to your app's `AndroidManifest.xml` file, replacing `myapp` with your redirect URI prefix if necessary: @@ -959,7 +992,7 @@ your redirect URI prefix if necessary: ``` -## Launch Social Web UI Sign In +### Launch Social Web UI Sign In Sweet! You're now ready to launch sign in with your social provider's web UI. @@ -1022,9 +1055,12 @@ RxAmplify.Auth.signInWithSocialWebUI(AuthProvider.facebook(), this) -## Update Info.plist -Sign-in with web UI requires the Amplify plugin to show up the sign-in UI inside a webview. After the sign-in process is complete it will redirect back to your app. +To sign in using an external identity provider such as Google, use the `signInWithWebUI` function. + +### Update Info.plist + +Sign-in with web UI requires the Amplify plugin to show up the sign-in UI inside a webview. After the sign-in process is complete it will redirect back to your app. You have to enable this in your app's `Info.plist`. Right click Info.plist and then choose Open As > Source Code. Add the following entry in the URL scheme: ```xml @@ -1052,7 +1088,7 @@ You have to enable this in your app's `Info.plist`. Right click Info.plist and t When creating a new SwiftUI app using Xcode 13 no longer require configuration files such as the Info.plist. If you are missing this file, click on the project target, under Info, Url Types, and click '+' to add a new URL Type. Add `myapp` to the URL Schemes. You should see the Info.plist file now with the entry for CFBundleURLSchemes. -## Launch Social Web UI Sign In +### Launch Social Web UI Sign In Invoke the following API with the provider you're using (shown with Facebook below): @@ -1099,4 +1135,842 @@ func socialSignInWithWebUI() -> AnyCancellable { -{/* sign-in with web UI (this is a very limited alternative to the Authenticator) */} + + + +## Sign in with passwordless methods + +Your application's users can also sign in using passwordless methods. To learn more, visit the [concepts page for passwordless](/[platform]/build-a-backend/auth/concepts/passwordless/). + +### SMS OTP + +{/* blurb with supplemental information about handling sign-in, events, etc. */} + + + +Pass `SMS_OTP` as the `preferredChallenge` when calling the `signIn` API in order to initiate a passwordless authentication flow with SMS OTP. + + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: '+15551234567', + options: { + authFlowType: 'USER_AUTH', + preferredChallenge: 'SMS_OTP', + }, +}); + +if (signInNextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_SMS_CODE') { + // prompt user for otp code delivered via SMS + const { nextStep: confirmSignInNextStep } = await confirmSignIn({ + challengeResponse: '123456', + }); + + if (confirmSignInNextStep.signInStep === 'DONE') { + console.log('Sign in successful!'); + } +} +``` + + + + +To request an OTP code via SMS for authentication, you pass the `challengeResponse` for `AuthFactorType.SMS_OTP` to the `confirmSignIn` API. + +Amplify will respond appropriately to Cognito and return the challenge as the sign in next step: `CONFIRM_SIGN_IN_WITH_OTP_CODE`. You will call `confirmSignIn` again, this time with the OTP that your user provides. + + + + +```java +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.SMS_OTP.getChallengeResponse(), + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); + +// Then pass that OTP into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "123456", + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.SMS_OTP.challengeResponse, + { result -> + if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) + +// Then pass that OTP into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "123456", + { result -> + // result.nextStep.signInStep should be "DONE" now + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) +``` + + + + +```kotlin +// First confirm the challenge type +var result = Amplify.Auth.confirmSignIn(AuthFactorType.SMS_OTP.challengeResponse) +if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP +} + +// Then pass that OTP into the confirmSignIn API +result = Amplify.Auth.confirmSignIn("123456") + +// result.nextStep.signInStep should be "DONE" now +``` + + + + +```java +// First confirm the challenge type +RxAmplify.Auth.confirmSignIn(AuthFactorType.SMS_OTP.getChallengeResponse()) + .subscribe( + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); + +// Then pass that OTP into the confirmSignIn API +RxAmplify.Auth.confirmSignIn("123456") + .subscribe( + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + + + + + +```swift +// sign in with `smsOTP` as preferred factor +func signIn(username: String) async { + do { + let pluginOptions = AWSAuthSignInOptions( + authFlowType: .userAuth(preferredFirstFactor: .smsOTP)) + let signInResult = try await Amplify.Auth.signIn( + username: username, + options: .init(pluginOptions: pluginOptions)) + print("Sign in succeeded. Next step: \(signInResult.nextStep)") + } catch let error as AuthError { + print("Sign in failed \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +// confirm sign in with the code received +func confirmSignIn() async { + do { + let signInResult = try await Amplify.Auth.confirmSignIn(challengeResponse: "") + print("Confirm sign in succeeded. Next step: \(signInResult.nextStep)") + } catch let error as AuthError { + print("Confirm sign in failed \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +``` + + + + +```swift +// sign in with `smsOTP` as preferred factor +func signIn(username: String) -> AnyCancellable { + Amplify.Publisher.create { + let pluginOptions = AWSAuthSignInOptions( + authFlowType: .userAuth(preferredFirstFactor: .smsOTP)) + try await Amplify.Auth.signIn( + username: username, + options: .init(pluginOptions: pluginOptions)) + }.sink { + if case let .failure(authError) = $0 { + print("Sign in failed \(authError)") + } + } + receiveValue: { signInResult in + print("Sign in succeeded. Next step: \(signInResult.nextStep)") + } +} + +// confirm sign in with the code received +func confirmSignIn() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.confirmSignIn(challengeResponse: "") + }.sink { + if case let .failure(authError) = $0 { + print("Confirm sign in failed \(authError)") + } + } + receiveValue: { signInResult in + print("Confirm sign in succeeded. Next step: \(signInResult.nextStep)") + } +} +``` + + + + + + +### Email OTP + +{/* blurb with supplemental information about handling sign-in, events, etc. */} + + + +Pass `EMAIL_OTP` as the `preferredChallenge` when calling the `signIn` API in order to initiate a passwordless authentication flow using email OTP. + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: 'hello@example.com', + options: { + authFlowType: 'USER_AUTH', + preferredChallenge: 'EMAIL_OTP', + }, +}); + +if (signInNextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE') { + // prompt user for otp code delivered via email + const { nextStep: confirmSignInNextStep } = await confirmSignIn({ + challengeResponse: '123456', + }); + + if (confirmSignInNextStep.signInStep === 'DONE') { + console.log('Sign in successful!'); + } +} +``` + + + + +To request an OTP code via email for authentication, you pass the `challengeResponse` for `AuthFactorType.EMAIL_OTP` to the `confirmSignIn` API. + +Amplify will respond appropriately to Cognito and return the challenge as the sign in next step: `CONFIRM_SIGN_IN_WITH_OTP_CODE`. You will call `confirmSignIn` again, this time with the OTP that your user provides. + + + + +```java +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.EMAIL_OTP.getChallengeResponse(), + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); + +// Then pass that OTP into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "123456", + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.EMAIL_OTP.challengeResponse, + { result -> + if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) + +// Then pass that OTP into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "123456", + { result -> + // result.nextStep.signInStep should be "DONE" now + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) +``` + + + + +```kotlin +// First confirm the challenge type +var result = Amplify.Auth.confirmSignIn(AuthFactorType.EMAIL_OTP.challengeResponse) +if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP +} + +// Then pass that OTP into the confirmSignIn API +result = Amplify.Auth.confirmSignIn("123456") + +// result.nextStep.signInStep should be "DONE" now +``` + + + + +```java +// First confirm the challenge type +RxAmplify.Auth.confirmSignIn(AuthFactorType.EMAIL_OTP.getChallengeResponse()) + .subscribe( + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP) { + // Show UI to collect OTP + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); + +// Then pass that OTP into the confirmSignIn API +RxAmplify.Auth.confirmSignIn("123456") + .subscribe( + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + + + + + + +```swift +// sign in with `emailOTP` as preferred factor +func signIn(username: String) async { + do { + let pluginOptions = AWSAuthSignInOptions( + authFlowType: .userAuth(preferredFirstFactor: .emailOTP)) + let signInResult = try await Amplify.Auth.signIn( + username: username, + options: .init(pluginOptions: pluginOptions)) + print("Sign in succeeded. Next step: \(signInResult.nextStep)") + } catch let error as AuthError { + print("Sign in failed \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +// confirm sign in with the code received +func confirmSignIn() async { + do { + let signInResult = try await Amplify.Auth.confirmSignIn(challengeResponse: "") + print("Confirm sign in succeeded. Next step: \(signInResult.nextStep)") + } catch let error as AuthError { + print("Confirm sign in failed \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +``` + + + + +```swift +// sign in with `emailOTP` as preferred factor +func signIn(username: String) -> AnyCancellable { + Amplify.Publisher.create { + let pluginOptions = AWSAuthSignInOptions( + authFlowType: .userAuth(preferredFirstFactor: .emailOTP)) + try await Amplify.Auth.signIn( + username: username, + options: .init(pluginOptions: pluginOptions)) + }.sink { + if case let .failure(authError) = $0 { + print("Sign in failed \(authError)") + } + } + receiveValue: { signInResult in + print("Sign in succeeded. Next step: \(signInResult.nextStep)") + } +} + +// confirm sign in with the code received +func confirmSignIn() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.confirmSignIn(challengeResponse: "") + }.sink { + if case let .failure(authError) = $0 { + print("Confirm sign in failed \(authError)") + } + } + receiveValue: { signInResult in + print("Confirm sign in succeeded. Next step: \(signInResult.nextStep)") + } +} +``` + + + + + + +### WebAuthn Passkeys + +{/* blurb with supplemental information about handling sign-in, events, etc. */} + + + +Pass `WEB_AUTHN` as the `preferredChallenge` in order to initiate the passwordless authentication flow using a WebAuthn credential. + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: 'hello@example.com', + options: { + authFlowType: 'USER_AUTH', + preferredChallenge: 'WEB_AUTHN', + }, +}); + +if (signInNextStep.signInStep === 'DONE') { + console.log('Sign in successful!'); +} +``` + + + + +To sign in with WebAuthn, you pass the `challengeResponse` for `AuthFactorType.WEB_AUTHN` to the `confirmSignIn` API. Amplify will invoke Android's Credential Manager to retrieve a PassKey, and the user will be shown a system UI to authorize the PassKey access. This flow +completes without any additional interaction from your application, so there is only one `confirmSignIn` call needed for WebAuthn. + + +Amplify requires an `Activity` reference to attach the PassKey UI to your Application's [Task](https://developer.android.com/guide/components/activities/tasks-and-back-stack) when using WebAuthn - if an `Activity` is not supplied then the UI will appear in a separate Task. For this reason, we strongly recommend passing the `callingActivity` option to both the `signIn` and `confirmSignIn` APIs if your application uses the `USER_AUTH` flow. + + + + + +```java +// Pass the calling activity +AuthSignInOptions options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build(); + +// Confirm WebAuthn as the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.WEB_AUTHN.getChallengeResponse(), + options, + result -> Log.i("AuthQuickStart", "Next sign in step: " + result.getNextStep()), + error -> Log.e("AuthQuickstart", "Failed to sign in", error) +); +``` + + + + +```kotlin +// Pass the calling activity +val options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build() + +// Confirm WebAuthn as the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.WEB_AUTHN.name, + options, + { result -> Log.i("AuthQuickStart", "Next sign in step: ${result.nextStep}") }, + { error -> Log.e("AuthQuickstart", "Failed to sign in", error) } +) +``` + + + + +```kotlin +// Pass the calling activity +val options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build() + +try { + // Confirm WebAuthn as the challenge type + var result = Amplify.Auth.confirmSignIn( + challengeResponse = AuthFactorType.WEB_AUTHN.challengeResponse, + options = options + ) + Log.i("AuthQuickStart", "Next sign in step: ${result.nextStep}") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Failed to sign in", error) +} +``` + + + + +```java +// Pass the calling activity +AuthSignInOptions options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build(); + +// Confirm WebAuthn as the challenge type +RxAmplify.Auth.confirmSignIn(AuthFactorType.WEB_AUTHN.getChallengeResponse(), options) + .subscribe( + result -> Log.i("AuthQuickStart", "Next sign in step: " + result.getNextStep()), + error -> Log.e("AuthQuickstart", "Failed to sign in", error) + ); +``` + + + + +Using WebAuthn sign in may result in a number of possible exception types. + +- `UserCancelledException` - If the user declines to authorize access to the PassKey in the system UI. You can retry the WebAuthn flow by invoking `confirmSignIn` again, or restart the `signIn` process to select a different `AuthFactorType`. +- `WebAuthnNotEnabledException` - This indicates WebAuthn is not enabled in your user pool. +- `WebAuthnNotSupportedException` - This indicates WebAuthn is not supported on the user's device. +- `WebAuthnRpMismatchException` - This indicates there is a problem with the `assetlinks.json` file deployed to your relying party. +- `WebAuthnFailedException` - This exception is used for other errors that may occur with WebAuthn. Inspect the `cause` to determine the best course of action. + + + + +{/* */} + + + + +{/* */} + + + + + +### Password + +Pass either `PASSWORD` or `PASSWORD_SRP` as the `preferredChallenge` in order to initiate a traditional password based authentication flow. + + + + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: 'hello@example.com', + password: 'example-password', + options: { + authFlowType: 'USER_AUTH', + preferredChallenge: 'PASSWORD_SRP', // or 'PASSWORD' + }, +}); + +if (confirmSignInNextStep.signInStep === 'DONE') { + console.log('Sign in successful!'); +} +``` + + + + + + + +```java +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.PASSWORD.getChallengeResponse(), // or PASSWORD_SRP + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD) { + // Show UI to collect password + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); + +// Then pass that password into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "password", + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// First confirm the challenge type +Amplify.Auth.confirmSignIn( + AuthFactorType.PASSWORD.challengeResponse, // or PASSWORD_SRP + { result -> + if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD) { + // Show UI to collect password + } + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) + +// Then pass that password into the confirmSignIn API +Amplify.Auth.confirmSignIn( + "password", + { result -> + // result.nextStep.signInStep should be "DONE" now + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) +``` + + + + +```kotlin +// First confirm the challenge type +var result = Amplify.Auth.confirmSignIn(AuthFactorType.PASSWORD.challengeResponse) // or PASSWORD_SRP +if (result.nextStep.signInStep == AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD) { + // Show UI to collect password +} + +// Then pass that password into the confirmSignIn API +result = Amplify.Auth.confirmSignIn("password") + +// result.nextStep.signInStep should be "DONE" now +``` + + + + +```java +// First confirm the challenge type +RxAmplify.Auth.confirmSignIn(AuthFactorType.PASSWORD.getChallengeResponse()) // or PASSWORD_SRP + .subscribe( + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONFIRM_SIGN_IN_WITH_PASSWORD) { + // Show UI to collect password + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); + +// Then pass that password into the confirmSignIn API +RxAmplify.Auth.confirmSignIn("password") + .subscribe( + result -> { + // result.getNextStep().getSignInStep() should be "DONE" now + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + +### First Factor Selection + +Omit the `preferredChallenge` parameter to discover what first factors are available for a given user. + +The `confirmSignIn` API can then be used to select a challenge and initiate the associated authentication flow. + + + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: '+15551234567', + options: { + authFlowType: 'USER_AUTH', + }, +}); + +if ( + signInNextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION' +) { + // present user with list of available challenges + console.log(`Available Challenges: ${signInNextStep.availableChallenges}`); + + // respond with user selection using `confirmSignIn` API + const { nextStep: nextConfirmSignInStep } = await confirmSignIn({ + challengeResponse: 'SMS_OTP', // or 'EMAIL_OTP', 'WEB_AUTHN', 'PASSWORD', 'PASSWORD_SRP' + }); +} + +``` + + + + + + +```java +// Retrieve the authentication factors by calling .availableFactors +AWSCognitoAuthSignInOptions options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .callingActivity(callingActivity) + .build(); +Amplify.Auth.signIn( + "hello@example.com", + null, + options, + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION) { + Log.i( + "AuthQuickstart", + "Available authentication factors for this user: " + result.getNextStep().getAvailableFactors() + ); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// Retrieve the authentication factors by calling .availableFactors +val options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_AUTH) + .callingActivity(callingActivity) + .build() +Amplify.Auth.signIn( + "hello@example.com", + null, + options, + { result -> + if (result.nextStep.signInStep == AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION) { + Log.i( + "AuthQuickstart", + "Available factors for this user: ${result.nextStep.availableFactors}" + ) + } + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) +``` + + + + +```kotlin +try { + // Retrieve the authentication factors by calling .availableFactors + val options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_AUTH) + .callingActivity(callingActivity) + .build() + val result = Amplify.Auth.signIn( + username = "hello@example.com", + password = null, + options = options + ) + if (result.nextStep.signInStep == AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION) { + Log.i( + "AuthQuickstart", + "Available factors for this user: ${result.nextStep.availableFactors}" + ) + } +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Sign in failed", error) +} +``` + + + + +```java +// Retrieve the authentication factors by calling .availableFactors +AWSCognitoAuthSignInOptions options = + AWSCognitoAuthSignInOptions + .builder() + .authFlowType(AuthFlowType.USER_AUTH) + .callingActivity(callingActivity) + .build(); +RxAmplify.Auth.signIn("hello@example.com", null, options) + .subscribe( + result -> { + if (result.getNextStep().getSignInStep() == AuthSignInStep.CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION) { + Log.i( + "AuthQuickstart", + "Available authentication factors for this user: " + result.getNextStep().getAvailableFactors() + ); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + + + diff --git a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-up/index.mdx b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-up/index.mdx index 692d8f82939..f5c72df2d6a 100644 --- a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-up/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/sign-up/index.mdx @@ -527,3 +527,870 @@ export default function App() { + + + +## Sign up with passwordless methods + +Your application's users can also sign up using passwordless methods. To learn more, visit the [concepts page for passwordless](/[platform]/build-a-backend/auth/concepts/passwordless/). + +### SMS OTP + +{/* blurb with supplemental information about handling sign-up, events, etc. */} + + + +```typescript +// Sign up using a phone number +const { nextStep: signUpNextStep } = await signUp({ + username: 'hello', + options: { + userAttributes: { + phone_number: '+15555551234', + }, + }, +}); + +if (signUpNextStep.signUpStep === 'DONE') { + console.log(`SignUp Complete`); +} + +if (signUpNextStep.signUpStep === 'CONFIRM_SIGN_UP') { + console.log( + `Code Delivery Medium: ${signUpNextStep.codeDeliveryDetails.deliveryMedium}`, + ); + console.log( + `Code Delivery Destination: ${signUpNextStep.codeDeliveryDetails.destination}`, + ); +} + +// Confirm sign up with the OTP received +const { nextStep: confirmSignUpNextStep } = await confirmSignUp({ + username: 'hello', + confirmationCode: '123456', +}); + +if (confirmSignUpNextStep.signUpStep === 'DONE') { + console.log(`SignUp Complete`); +} +``` + + + + + + + +```java +// Sign up using a phone number +ArrayList attributes = new ArrayList<>(); +attributes.add(new AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+15551234567")); + +Amplify.Auth.signUp( + "hello@example.com", + null, + AuthSignUpOptions.builder().userAttributes(attributes).build(), + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } else if (result.getNextStep().getSignUpStep() == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + result.getNextStep().getCodeDeliveryDetails().getDeliveryMedium()); + Log.i("AuthQuickstart", "Code Deliver Destination: " + + result.getNextStep().getCodeDeliveryDetails().getDestination()); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); + +// Confirm sign up with the OTP received +Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456", + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// Sign up using a phone number +val attributes = listOf( + AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+15555551234") +) +val options = + AuthSignUpOptions + .builder() + .userAttributes(attributes) + .build() + +Amplify.Auth.signUp( + "hello@example.com", + null, + options, + { result -> + if (result.isSignUpComplete) { + Log.i("AuthQuickstart", "Sign up is complete") + } else if (result.nextStep.signUpStep == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + "${result.nextStep.codeDeliveryDetails?.deliveryMedium}") + Log.i("AuthQuickstart", "Code Deliver Destination: " + + "${result.nextStep.codeDeliveryDetails?.destination}") + } + }, + { Log.e("AuthQuickstart", "Failed to sign up", it) } +) + +// Confirm sign up with the OTP received +Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456", + { result -> + if (result.nextStep.signUpStep == AuthSignUpStep.DONE) { + Log.i("AuthQuickstart", "Sign up is complete") + } + }, + { Log.e("AuthQuickstart", "Failed to sign up", it) } +) +``` + + + + +```kotlin +// Sign up using a phone number +val attributes = listOf( + AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+15555551234") +) +val options = + AuthSignUpOptions + .builder() + .userAttributes(attributes) + .build() +var result = Amplify.Auth.signUp("hello@example.com", null, options) + +if (result.isSignUpComplete) { + Log.i("AuthQuickstart", "Sign up is complete") +} else if (result.nextStep.signUpStep == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + "${result.nextStep.codeDeliveryDetails?.deliveryMedium}") + Log.i("AuthQuickstart", "Code Deliver Destination: " + + "${result.nextStep.codeDeliveryDetails?.destination}") +} + +// Confirm sign up with the OTP received +result = Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456" +) + +if (result.nextStep.signUpStep == AuthSignUpStep.DONE) { + Log.i("AuthQuickstart", "Sign up is complete") +} +``` + + + + +```java +// Sign up using a phone number +ArrayList attributes = new ArrayList<>(); +attributes.add(new AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+15551234567")); + +RxAmplify.Auth.signUp( + "hello@example.com", + null, + AuthSignUpOptions.builder().userAttributes(attributes).build() +) + .subscribe( + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } else if (result.getNextStep().getSignUpStep() == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + result.getNextStep().getCodeDeliveryDetails().getDeliveryMedium()); + Log.i("AuthQuickstart", "Code Deliver Destination: " + + result.getNextStep().getCodeDeliveryDetails().getDestination()); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); + +// Confirm sign up with the OTP received +RxAmplify.Auth.confirmSignUp( + "hello@example.com", + "123456" +) + .subscribe( + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + + + + + + +```swift +// Sign up using an phone number +func signUp(username: String, phonenumber: String) async { + let userAttributes = [ + AuthUserAttribute(.phoneNumber, value: phonenumber) + ] + let options = AuthSignUpRequest.Options(userAttributes: userAttributes) + do { + let signUpResult = try await Amplify.Auth.signUp( + username: username, + options: options + ) + if case let .confirmUser(deliveryDetails, _, userId) = signUpResult.nextStep { + print("Delivery details \(String(describing: deliveryDetails)) for userId: \(String(describing: userId)))") + } else { + print("SignUp Complete") + } + } catch let error as AuthError { + print("An error occurred while registering a user \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +// Confirm sign up with the OTP received +func confirmSignUp(for username: String, with confirmationCode: String) async { + do { + let confirmSignUpResult = try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + print("Confirm sign up result completed: \(confirmSignUpResult.isSignUpComplete)") + } catch let error as AuthError { + print("An error occurred while confirming sign up \(error)") + } catch { + print("Unexpected error: \(error)") + } +} +``` + + + + + +```swift +// Sign up using a phone number +func signUp(username: String, phonenumber: String) -> AnyCancellable { + let userAttributes = [ + AuthUserAttribute(.phoneNumber, value: phonenumber) + ] + let options = AuthSignUpRequest.Options(userAttributes: userAttributes) + let sink = Amplify.Publisher.create { + try await Amplify.Auth.signUp( + username: username, + options: options + ) + }.sink { + if case let .failure(authError) = $0 { + print("An error occurred while registering a user \(authError)") + } + } + receiveValue: { signUpResult in + if case let .confirmUser(deliveryDetails, _, userId) = signUpResult.nextStep { + print("Delivery details \(String(describing: deliveryDetails)) for userId: \(String(describing: userId)))") + } else { + print("SignUp Complete") + } + } + return sink +} + +// Confirm sign up with the OTP received +func confirmSignUp(for username: String, with confirmationCode: String) -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + }.sink { + if case let .failure(authError) = $0 { + print("An error occurred while confirming sign up \(authError)") + } + } + receiveValue: { _ in + print("Confirm signUp succeeded") + } +} +``` + + + + + + + +### Email OTP + +{/* blurb with supplemental information about handling sign-up, events, etc. */} + + + +```typescript +// Sign up using an email address +const { nextStep: signUpNextStep } = await signUp({ + username: 'hello', + options: { + userAttributes: { + email: 'hello@example.com', + }, + }, +}); + +if (signUpNextStep.signUpStep === 'DONE') { + console.log(`SignUp Complete`); +} + +if (signUpNextStep.signUpStep === 'CONFIRM_SIGN_UP') { + console.log( + `Code Delivery Medium: ${signUpNextStep.codeDeliveryDetails.deliveryMedium}`, + ); + console.log( + `Code Delivery Destination: ${signUpNextStep.codeDeliveryDetails.destination}`, + ); +} + +// Confirm sign up with the OTP received +const { nextStep: confirmSignUpNextStep } = await confirmSignUp({ + username: 'hello', + confirmationCode: '123456', +}); + +if (confirmSignUpNextStep.signUpStep === 'DONE') { + console.log(`SignUp Complete`); +} +``` + + + + + + + +```java +// Sign up using an email address +ArrayList attributes = new ArrayList<>(); +attributes.add(new AuthUserAttribute(AuthUserAttributeKey.email(), "hello@example.com")); + +Amplify.Auth.signUp( + "hello@example.com", + null, + AuthSignUpOptions.builder().userAttributes(attributes).build(), + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } else if (result.getNextStep().getSignUpStep() == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + result.getNextStep().getCodeDeliveryDetails().getDeliveryMedium()); + Log.i("AuthQuickstart", "Code Deliver Destination: " + + result.getNextStep().getCodeDeliveryDetails().getDestination()); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); + +// Confirm sign up with the OTP received +Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456", + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) +); +``` + + + + +```kotlin +// Sign up using an email address +val attributes = listOf( + AuthUserAttribute(AuthUserAttributeKey.email(), "my@email.com") +) +val options = + AuthSignUpOptions + .builder() + .userAttributes(attributes) + .build() + +Amplify.Auth.signUp( + "hello@example.com", + null, + options, + { result -> + if (result.isSignUpComplete) { + Log.i("AuthQuickstart", "Sign up is complete") + } else if (result.nextStep.signUpStep == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + "${result.nextStep.codeDeliveryDetails?.deliveryMedium}") + Log.i("AuthQuickstart", "Code Deliver Destination: " + + "${result.nextStep.codeDeliveryDetails?.destination}") + } + }, + { Log.e("AuthQuickstart", "Failed to sign up", it) } +) + +// Confirm sign up with the OTP received +Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456", + { result -> + if (result.nextStep.signUpStep == AuthSignUpStep.DONE) { + Log.i("AuthQuickstart", "Sign up is complete") + } + }, + { Log.e("AuthQuickstart", "Failed to sign up", it) } +) +``` + + + + +```kotlin +// Sign up using an email address +val attributes = listOf( + AuthUserAttribute(AuthUserAttributeKey.email(), "my@email.com") +) +val options = + AuthSignUpOptions + .builder() + .userAttributes(attributes) + .build() +var result = Amplify.Auth.signUp("hello@example.com", null, options) + +if (result.isSignUpComplete) { + Log.i("AuthQuickstart", "Sign up is complete") +} else if (result.nextStep.signUpStep == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + "${result.nextStep.codeDeliveryDetails?.deliveryMedium}") + Log.i("AuthQuickstart", "Code Deliver Destination: " + + "${result.nextStep.codeDeliveryDetails?.destination}") +} + +// Confirm sign up with the OTP received +result = Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456" +) + +if (result.nextStep.signUpStep == AuthSignUpStep.DONE) { + Log.i("AuthQuickstart", "Sign up is complete") +} +``` + + + + +```java +// Sign up using an email address +ArrayList attributes = new ArrayList<>(); +attributes.add(new AuthUserAttribute(AuthUserAttributeKey.email(), "my@email.com")); + +RxAmplify.Auth.signUp( + "hello@example.com", + null, + AuthSignUpOptions.builder().userAttributes(attributes).build() +) + .subscribe( + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } else if (result.getNextStep().getSignUpStep() == AuthSignUpStep.CONFIRM_SIGN_UP_STEP) { + Log.i("AuthQuickstart", "Code Deliver Medium: " + + result.getNextStep().getCodeDeliveryDetails().getDeliveryMedium()); + Log.i("AuthQuickstart", "Code Deliver Destination: " + + result.getNextStep().getCodeDeliveryDetails().getDestination()); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); + +// Confirm sign up with the OTP received +RxAmplify.Auth.confirmSignUp( + "hello@example.com", + "123456" +) + .subscribe( + result -> { + if (result.isSignUpComplete()) { + Log.i("AuthQuickstart", "Sign up is complete"); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + + + + + + + + +```swift +// Sign up using an email +func signUp(username: String, email: String) async { + let userAttributes = [ + AuthUserAttribute(.email, value: email) + ] + let options = AuthSignUpRequest.Options(userAttributes: userAttributes) + do { + let signUpResult = try await Amplify.Auth.signUp( + username: username, + options: options + ) + if case let .confirmUser(deliveryDetails, _, userId) = signUpResult.nextStep { + print("Delivery details \(String(describing: deliveryDetails)) for userId: \(String(describing: userId)))") + } else { + print("SignUp Complete") + } + } catch let error as AuthError { + print("An error occurred while registering a user \(error)") + } catch { + print("Unexpected error: \(error)") + } +} + +// Confirm sign up with the OTP received +func confirmSignUp(for username: String, with confirmationCode: String) async { + do { + let confirmSignUpResult = try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + print("Confirm sign up result completed: \(confirmSignUpResult.isSignUpComplete)") + } catch let error as AuthError { + print("An error occurred while confirming sign up \(error)") + } catch { + print("Unexpected error: \(error)") + } +} +``` + + + + + +```swift +// Sign up using an email +func signUp(username: String, email: String) -> AnyCancellable { + let userAttributes = [ + AuthUserAttribute(.email, value: email) + ] + let options = AuthSignUpRequest.Options(userAttributes: userAttributes) + let sink = Amplify.Publisher.create { + try await Amplify.Auth.signUp( + username: username, + options: options + ) + }.sink { + if case let .failure(authError) = $0 { + print("An error occurred while registering a user \(authError)") + } + } + receiveValue: { signUpResult in + if case let .confirmUser(deliveryDetails, _, userId) = signUpResult.nextStep { + print("Delivery details \(String(describing: deliveryDetails)) for userId: \(String(describing: userId)))") + } else { + print("SignUp Complete") + } + } + return sink +} + +// Confirm sign up with the OTP received +func confirmSignUp(for username: String, with confirmationCode: String) -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + }.sink { + if case let .failure(authError) = $0 { + print("An error occurred while confirming sign up \(authError)") + } + } + receiveValue: { _ in + print("Confirm signUp succeeded") + } +} +``` + + + + + + + +### Auto Sign In + +{/* blurb with supplemental information about auto sign in */} + + + +```typescript +// Call `signUp` API with `USER_AUTH` as the authentication flow type for `autoSignIn` +const { nextStep: signUpNextStep } = await signUp({ + username: 'hello', + options: { + userAttributes: { + email: 'hello@example.com', + phone_number: '+15555551234', + }, + autoSignIn: { + authFlowType: 'USER_AUTH', + }, + }, +}); + +if (signUpNextStep.signUpStep === 'CONFIRM_SIGN_UP') { + console.log( + `Code Delivery Medium: ${signUpNextStep.codeDeliveryDetails.deliveryMedium}`, + ); + console.log( + `Code Delivery Destination: ${signUpNextStep.codeDeliveryDetails.destination}`, + ); +} + +// Call `confirmSignUp` API with the OTP received +const { nextStep: confirmSignUpNextStep } = await confirmSignUp({ + username: 'hello', + confirmationCode: '123456', +}); + +if (confirmSignUpNextStep.signUpStep === 'COMPLETE_AUTO_SIGN_IN') { + // Call `autoSignIn` API to complete the flow + const { nextStep } = await autoSignIn(); + + if (nextStep.signInStep === 'DONE') { + console.log('Successfully signed in.'); + } +} + +``` + + + + + + +```java +private void confirmSignUp(String username, String confirmationCode) { + // Confirm sign up with the OTP received then auto sign in + Amplify.Auth.confirmSignUp( + username, + confirmationCode, + result -> { + if (result.getNextStep().getSignUpStep() == AuthSignUpStep.COMPLETE_AUTO_SIGN_IN) { + Log.i("AuthQuickstart", "Sign up is complete, auto sign in"); + autoSignIn(); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +} + +private void autoSignIn() { + Amplify.Auth.autoSignIn( + result -> Log.i("AuthQuickstart", "Sign in is complete"), + error -> Log.e("AuthQuickstart", error.toString()) + ); +} +``` + + + + +```kotlin +fun confirmSignUp(username: String, confirmationCode: String) { + // Confirm sign up with the OTP received + Amplify.Auth.confirmSignUp( + username, + confirmationCode, + { signUpResult -> + if (signUpResult.nextStep.signUpStep == AuthSignUpStep.COMPLETE_AUTO_SIGN_IN) { + Log.i("AuthQuickstart", "Sign up is complete, auto sign in") + autoSignIn() + } + }, + { Log.e("AuthQuickstart", "Failed to sign up", it) } + ) +} +fun autoSignIn() { + Amplify.Auth.autoSignIn( + { signInResult -> + Log.i("AuthQuickstart", "Sign in is complete") + }, + { Log.e("AuthQuickstart", "Failed to sign in", it) } + ) +} +``` + + + + +```kotlin +suspend fun confirmSignUp(username: String, confirmationCode: String) { + // Confirm sign up with the OTP received then auto sign in + val result = Amplify.Auth.confirmSignUp( + "hello@example.com", + "123456" + ) + + if (result.nextStep.signUpStep == AuthSignUpStep.COMPLETE_AUTO_SIGN_IN) { + Log.i("AuthQuickstart", "Sign up is complete, auto sign in") + autoSignIn() + } +} + +suspend fun autoSignIn() { + val result = Amplify.Auth.autoSignIn() + if (result.isSignedIn) { + Log.i("AuthQuickstart", "Sign in is complete") + } else { + Log.e("AuthQuickstart", "Sign in did not complete $result") + } +} +``` + + + + +```java +private void confirmSignUp(String username, String confirmationCode) { + // Confirm sign up with the OTP received then auto sign in + RxAmplify.Auth.confirmSignUp( + username, + confirmationCode + ) + .subscribe( + result -> { + if (result.getNextStep().getSignUpStep() == AuthSignUpStep.COMPLETE_AUTO_SIGN_IN) { + Log.i("AuthQuickstart", "Sign up is complete, auto sign in"); + autoSignIn(); + } + }, + error -> Log.e("AuthQuickstart", error.toString()) + ); +} + +private void autoSignIn() { + RxAmplify.Auth.autoSignIn() + .subscribe( + result -> Log.i("AuthQuickstart", "Sign in is complete" + result.toString()), + error -> Log.e("AuthQuickstart", error.toString()) + ); +} +``` + + + + + + + + + + + +```swift +// Confirm sign up with the OTP received and auto sign in +func confirmSignUp(for username: String, with confirmationCode: String) async { + do { + let confirmSignUpResult = try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + if case .completeAutoSignIn(let session) = confirmSignUpResult.nextStep { + let autoSignInResult = try await Amplify.Auth.autoSignIn() + print("Auto sign in result: \(autoSignInResult.isSignedIn)") + } else { + print("Confirm sign up result completed: \(confirmSignUpResult.isSignUpComplete)") + } + } catch let error as AuthError { + print("An error occurred while confirming sign up \(error)") + } catch { + print("Unexpected error: \(error)") + } +} +``` + + + + + +```swift +// Confirm sign up with the OTP received and auto sign in +func confirmSignUp(for username: String, with confirmationCode: String) -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.confirmSignUp( + for: username, + confirmationCode: confirmationCode + ) + }.sink { + if case let .failure(authError) = $0 { + print("An error occurred while confirming sign up \(authError)") + } + } + receiveValue: { confirmSignUpResult in + if case let .completeAutoSignIn(session) = confirmSignUpResult.nextStep { + print("Confirm Sign Up succeeded. Next step is auto sign in") + // call `autoSignIn()` API to complete sign in + } else { + print("Confirm sign up result completed: \(confirmSignUpResult.isSignUpComplete)") + } + } +} + +func autoSignIn() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.autoSignIn() + }.sink { + if case let .failure(authError) = $0 { + print("Auto Sign in failed \(authError)") + } + } + receiveValue: { autoSignInResult in + if autoSignInResult.isSignedIn { + print("Auto Sign in succeeded") + } + } +} +``` + + + + + + + + diff --git a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/switching-authentication-flows/index.mdx b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/switching-authentication-flows/index.mdx index dbddf87f4bb..d08f0c15f9a 100644 --- a/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/switching-authentication-flows/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/connect-your-frontend/switching-authentication-flows/index.mdx @@ -10,7 +10,8 @@ export const meta = { 'react', 'react-native', 'swift', - 'vue' + 'vue', + 'android' ] }; @@ -28,7 +29,7 @@ export function getStaticProps() { -`AWSCognitoAuthPlugin` allows you to switch between different auth flows while initiating signIn. You can configure the flow in the `amplifyconfiguration.json` file or pass the `authFlowType` as a runtime parameter to the `signIn` api call. +`AWSCognitoAuthPlugin` allows you to switch between different auth flows while initiating signIn. You can configure the flow in the `amplify_outputs.json` file or pass the `authFlowType` as a runtime parameter to the `signIn` api call. For client side authentication there are four different flows that can be configured during runtime: @@ -40,6 +41,8 @@ For client side authentication there are four different flows that can be config 4. `customWithoutSRP`: The `customWithoutSRP` flow is used to start authentication flow **WITHOUT** SRP and then use a series of challenge and response cycles that can be customized to meet different requirements. +5. `userAuth`: The `userAuth` flow is a choice-based authentication flow that allows the user to choose from the list of available authentication methods. This flow is useful when you want to provide the user with the option to choose the authentication method. The choices that may be available to the user are `emailOTP`, `smsOTP`, `webAuthn`, `password` or `passwordSRP`. + `Auth` can be configured to use the different flows at runtime by calling `signIn` with `AuthSignInOptions`'s `authFlowType` as `AuthFlowType.userPassword`, `AuthFlowType.customAuthWithoutSrp` or `AuthFlowType.customAuthWithSrp`. If you do not specify the `AuthFlowType` in `AuthSignInOptions`, the default flow (`AuthFlowType.userSRP`) will be used. @@ -50,6 +53,31 @@ Runtime configuration will take precedence and will override any auth flow type > For more information about authentication flows, please visit [Amazon Cognito developer documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html#amazon-cognito-user-pools-custom-authentication-flow) +## USER_AUTH (Choice-based authentication) flow + +A use case for the `USER_AUTH` authentication flow is to provide the user with the option to choose the authentication method. The choices that may be available to the user are `emailOTP`, `smsOTP`, `webAuthn`, `password` or `passwordSRP`. + +```swift +let pluginOptions = AWSAuthSignInOptions( + authFlowType: .userAuth) +let signInResult = try await Amplify.Auth.signIn( + username: username, + password: password, + options: .init(pluginOptions: pluginOptions)) +guard case .continueSignInWithFirstFactorSelection(let availableFactors) = signInResult.nextStep else { + return +} +print("Available factors: \(availableFactors)") +``` + +The selection of the authentication method is done by the user. The user can choose from the available factors and proceed with the selected factor. You should call the `confirmSignIn` API with the selected factor to continue the sign-in process. Following is an example if you want to proceed with the `emailOTP` factor selection: + +```swift +// Select emailOTP as the factor +var confirmSignInResult = try await Amplify.Auth.confirmSignIn( + challengeResponse: AuthFactorType.emailOTP.challengeResponse) +``` + ## USER_PASSWORD_AUTH flow A use case for the `USER_PASSWORD_AUTH` authentication flow is migrating users into Amazon Cognito @@ -74,29 +102,6 @@ func signIn(username: String, password: String) async throws { } ``` -### Set up auth backend - -In order to use the authentication flow `USER_PASSWORD_AUTH`, your Cognito app client has to be configured to allow it. Amplify Gen 2 enables SRP auth by default. To enable USER_PASSWORD_AUTH, you can update the `backend.ts` file with the following changes: - -```ts title="amplify/backend.ts" -import { defineBackend } from '@aws-amplify/backend' -import { auth } from './auth/resource' -import { data } from './data/resource' - -const backend = defineBackend({ - auth, - data, -}); - -// highlight-start -backend.auth.resources.cfnResources.cfnUserPoolClient.explicitAuthFlows = [ - "ALLOW_USER_PASSWORD_AUTH", - "ALLOW_USER_SRP_AUTH", - "ALLOW_REFRESH_TOKEN_AUTH" -]; -// highlight-end -``` - ### Migrate users with Amazon Cognito Amazon Cognito provides a trigger to migrate users from your existing user directory seamlessly into Cognito. You achieve this by configuring your User Pool's "Migration" trigger which invokes a Lambda function whenever a user that does not already exist in the user pool authenticates, or resets their password. @@ -117,7 +122,7 @@ Follow the instructions in [Custom Auth Sign In](/gen1/[platform]/build-a-backen -For client side authentication there are three different flows: +For client side authentication there are four different flows: 1. `USER_SRP_AUTH`: The `USER_SRP_AUTH` flow uses the [SRP protocol (Secure Remote Password)](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) where the password never leaves the client and is unknown to the server. This is the recommended flow and is used by default. @@ -125,20 +130,85 @@ For client side authentication there are three different flows: 3. `CUSTOM_WITH_SRP` & `CUSTOM_WITHOUT_SRP`: Allows for a series of challenge and response cycles that can be customized to meet different requirements. +4. `USER_AUTH`: The `USER_AUTH` flow is a choice-based authentication flow that allows the user to choose from the list of available authentication methods. This flow is useful when you want to provide the user with the option to choose the authentication method. The choices that may be available to the user are `EMAIL_OTP`, `SMS_OTP`, `WEB_AUTHN`, `PASSWORD` or `PASSWORD_SRP`. + The Auth flow can be customized when calling `signIn`, for example: ```ts title="src/main.ts" await signIn({ - username: "hello@mycompany.com", + username: "hello@mycompany.com", password: "hunter2", options: { - authFlowType: 'USER_PASSWORD_AUTH' + authFlowType: 'USER_AUTH' } }) ``` > For more information about authentication flows, please visit [AWS Cognito developer documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html#amazon-cognito-user-pools-custom-authentication-flow) +## USER_AUTH flow + +The `USER_AUTH` sign in flow supports the following methods as first factors for authentication: `WEB_AUTHN`, `EMAIL_OTP`, `SMS_OTP`, `PASSWORD`, and `PASSWORD_SRP`. + +If the desired first factor is known when authentication is initiated, it can be passed to the `signIn` API as the `preferredChallenge` to initiate the corresponding authentication flow. + +```ts +// PASSWORD_SRP / PASSWORD +// sign in with preferred challenge as password +// note password must be provided in same step +const { nextStep } = await signIn({ + username: "hello@mycompany.com", + password: "hunter2", + options: { + authFlowType: "USER_AUTH", + preferredChallenge: "PASSWORD_SRP" // or "PASSWORD" + }, +}); + +// WEB_AUTHN / EMAIL_OTP / SMS_OTP +// sign in with preferred passwordless challenge +// no additional user input required at this step +const { nextStep } = await signIn({ + username: "hello@example.com", + options: { + authFlowType: "USER_AUTH", + preferredChallenge: "WEB_AUTHN" // or "EMAIL_OTP" or "SMS_OTP" + }, +}); +``` + +If the desired first factor is not known or you would like to provide users with the available options, `preferredChallenge` can be omitted from the initial `signIn` API call. + +This allows you to discover which authentication first factors are available for a user via the `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` step. You can then present the available options to the user and use the `confirmSignIn` API to respond with the user's selection. + +```ts +const { nextStep: signInNextStep } = await signIn({ + username: '+15551234567', + options: { + authFlowType: 'USER_AUTH', + }, +}); + +if ( + signInNextStep.signInStep === 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION' +) { + // present user with list of available challenges + console.log(`Available Challenges: ${signInNextStep.availableChallenges}`); + + // respond with user selection using `confirmSignIn` API + const { nextStep: nextConfirmSignInStep } = await confirmSignIn({ + challengeResponse: 'SMS_OTP', // or 'EMAIL_OTP', 'WEB_AUTHN', 'PASSWORD', 'PASSWORD_SRP' + }); +} + +``` +Also, note that if the `preferredChallenge` passed to the initial `signIn` API call is unavailable for the user, Amplify will also respond with the `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION` next step. + + + +For more information about determining a first factor, and signing in with passwordless authentication factors, please visit the [Passwordless](/[platform]/build-a-backend/auth/concepts/passwordless/) concepts page. + + ## USER_PASSWORD_AUTH flow A use case for the `USER_PASSWORD_AUTH` authentication flow is migrating users into Amazon Cognito @@ -200,6 +270,387 @@ To create a CAPTCHA challenge with a Lambda Trigger, please visit [AWS Amplify G + + +`AWSCognitoAuthPlugin` allows you to switch between different auth flows while initiating signIn. You can configure the flow in the `amplify_outputs.json` file or pass the `authFlowType` as a option to the `signIn` api call. + +For client side authentication there are four different flows that can be configured during runtime: + +1. `USER_SRP_AUTH`: The `USER_SRP_AUTH` flow uses the [SRP protocol (Secure Remote Password)](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) where the password never leaves the client and is unknown to the server. This is the recommended flow and is used by default. + +2. `USER_PASSWORD_AUTH`: The `USER_PASSWORD_AUTH` flow will send user credentials unencrypted to the back-end. If you want to migrate users to Cognito using the "Migration" trigger and avoid forcing users to reset their passwords, you will need to use this authentication type because the Lambda function invoked by the trigger needs to verify the supplied credentials. + +3. `CUSTOM_AUTH_WITH_SRP`: The `CUSTOM_AUTH_WITH_SRP` flow is used to start with SRP authentication and then switch to custom authentication. This is useful if you want to use SRP for the initial authentication and then use custom authentication for subsequent authentication attempts. + +4. `CUSTOM_AUTH_WITHOUT_SRP`: The `CUSTOM_AUTH_WITHOUT_SRP` flow is used to start authentication flow **WITHOUT** SRP and then use a series of challenge and response cycles that can be customized to meet different requirements. + +5. `USER_AUTH`: The `USER_AUTH` flow is a choice-based authentication flow that allows the user to choose from the list of available authentication methods. This flow is useful when you want to provide the user with the option to choose the authentication method. The choices that may be available to the user are `EMAIL_OTP`, `SMS_OTP`, `WEB_AUTHN`, `PASSWORD` or `PASSWORD_SRP`. + +`Auth` can be configured to use the different flows at runtime by calling `signIn` with `AWSCognitoAuthSignInOptions`'s `authFlowType` as `AuthFlowType.USER_PASSWORD_AUTH`, `AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP`, `AuthFlowType.CUSTOM_AUTH_WITH_SRP`, or `AuthFlowType.USER_AUTH`. If you do not specify the `AuthFlowType` in `AWSCognitoAuthSignInOptions`, the default flow specified in `amplify_outputs.json` will be used. + + + +Runtime configuration will take precedence and will override any auth flow type configuration present in `amplify_outputs.json`. + + + +For more information about authentication flows, please visit [Amazon Cognito developer documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html#amazon-cognito-user-pools-custom-authentication-flow) + +## USER_AUTH (Choice-based authentication) flow + +A use case for the `USER_AUTH` authentication flow is to provide the user with the option to choose the authentication method. The choices that may be available to the user are `EMAIL_OTP`, `SMS_OTP`, `WEB_AUTHN`, `PASSWORD` or `PASSWORD_SRP`. + + +Amplify requires an `Activity` reference to attach the PassKey UI to your Application's [Task](https://developer.android.com/guide/components/activities/tasks-and-back-stack) when using WebAuthn - if an `Activity` is not supplied then the UI will appear in a separate Task. For this reason, we strongly recommend passing the `callingActivity` option to both the `signIn` and `confirmSignIn` APIs if your application uses the `USER_AUTH` flow. + + +If the desired first factor is known before the sign in flow is initiated it can be passed to the initial sign in call. + + + + +```java +// PASSWORD_SRP / PASSWORD +// Sign in with preferred challenge as password +// NOTE: Password must be provided in the same step +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD_SRP) // or "PASSWORD" + .build(); +Amplify.Auth.signIn( + username, + password, + options, + result -> Log.i("AuthQuickStart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) +); + +// WEB_AUTHN / EMAIL_OTP / SMS_OTP +// Sign in with preferred passwordless challenge +// No user input is required at this step +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.WEB_AUTHN) // or "EMAIL_OTP" or "SMS_OTP" + .build(); +Amplify.Auth.signIn( + username, + null, + options, + result -> Log.i("AuthQuickStart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) +); +``` + + + + +```kotlin +// PASSWORD_SRP / PASSWORD +// Sign in with preferred challenge as password +// NOTE: Password must be provided in the same step +val options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD_SRP) // or "PASSWORD" + .build() +Amplify.Auth.signIn( + username, + password, + options, + { result -> Log.i("AuthQuickStart", "Next step for sign in is ${result.nextStep}") }, + { error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) } +) + +// WEB_AUTHN / EMAIL_OTP / SMS_OTP +// Sign in with preferred passwordless challenge +// No user input is required at this step +val options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.WEB_AUTHN) // or "EMAIL_OTP" or "SMS_OTP" + .build() +Amplify.Auth.signIn( + username, + null, + options, + { result -> Log.i("AuthQuickStart", "Next step for sign in is ${result.nextStep}") }, + { error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) } +) +``` + + + + +```kotlin +// PASSWORD_SRP / PASSWORD +// Sign in with preferred challenge as password +// NOTE: Password must be provided in the same step +try { + val options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD_SRP) // or "PASSWORD" + .build() + val result = Amplify.Auth.signIn( + username = "hello@example.com", + password = "password", + options = options + ) + Log.i("AuthQuickstart", "Next step for sign in is ${result.nextStep}") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Sign in failed", error) +} + +// WEB_AUTHN / EMAIL_OTP / SMS_OTP +// Sign in with preferred passwordless challenge +// No user input is required at this step +try { + val options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.WEB_AUTHN) // or "EMAIL_OTP" or "SMS_OTP" + .build() + val result = Amplify.Auth.signIn( + username = "hello@example.com", + password = null, + options = options + ) + Log.i("AuthQuickstart", "Next step for sign in is ${result.nextStep}") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Sign in failed", error) +} +``` + + + + +```java +// PASSWORD_SRP / PASSWORD +// Sign in with preferred challenge as password +// NOTE: Password must be provided in the same step +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.PASSWORD_SRP) // or "PASSWORD" + .build(); +RxAmplify.Auth.signIn(username, password, options) + .subscribe( + result -> Log.i("AuthQuickstart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickstart", "Failed to confirm sign in", error)) + ); + +// WEB_AUTHN / EMAIL_OTP / SMS_OTP +// Sign in with preferred passwordless challenge +// No user input is required at this step +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .callingActivity(activity) + .authFlowType(AuthFlowType.USER_AUTH) + .preferredFirstFactor(AuthFactorType.WEB_AUTHN) // or "EMAIL_OTP" or "SMS_OTP" + .build(); +RxAmplify.Auth.signIn(username, null, options) + .subscribe( + result -> Log.i("AuthQuickstart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickstart", "Failed to confirm sign in", error)) + ); +``` + + + + +If the preferred first factor is not supplied or is unavailable, and the user has multiple factors available, the flow will continue to select an available first factor by returning an `AuthNextSignInStep.signInStep` value of `CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION`, and a list of `AuthNextSignInStep.availableFactors`. + +The selection of the authentication method is done by the user. The user can choose from the available factors and proceed with the selected factor. You should call the `confirmSignIn` API with the selected factor to continue the sign-in process. Following is an example if you want to proceed with the `WEB_AUTHN` factor selection: + + + + +```java +AuthConfirmSignInOptions options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build(); +Amplify.Auth.confirmSignIn( + AuthFactorType.WEB_AUTHN.getChallengeResponse(), + options, + result -> Log.i("AuthQuickStart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) +); +``` + + + + +```kotlin +val options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build() +Amplify.Auth.confirmSignIn( + AuthFactorType.WEB_AUTHN.challengeResponse, + options, + { result -> Log.i("AuthQuickStart", "Next step for sign in is ${result.nextStep}") }, + { error -> Log.e("AuthQuickStart", "Failed to confirm sign in", error) } +) +``` + + + + +```kotlin +try { + val options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build() + val result = Amplify.Auth.confirmSignIn( + challengeResponse = AuthFactorType.WEB_AUTHN.challengeResponse, + options = options + ) + Log.i("AuthQuickstart", "Next step for sign in is ${result.nextStep}") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Sign in failed", error) +} +``` + + + + +```java +AuthConfirmSignInOptions options = AWSCognitoAuthConfirmSignInOptions.builder() + .callingActivity(activity) + .build(); +RxAmplify.Auth.confirmSignIn(AuthFactorType.WEB_AUTHN.getChallengeResponse(), options) + .subscribe( + result -> Log.i("AuthQuickstart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickstart", "Failed to confirm sign in", error)) + ); +``` + + + + +## USER_PASSWORD_AUTH flow + +A use case for the `USER_PASSWORD_AUTH` authentication flow is migrating users into Amazon Cognito + +A user migration Lambda trigger helps migrate users from a legacy user management system into your user pool. If you choose the USER_PASSWORD_AUTH authentication flow, users don't have to reset their passwords during user migration. This flow sends your user's password to the service over an encrypted SSL connection during authentication. + +When you have migrated all your users, switch flows to the more secure SRP flow. The SRP flow doesn't send any passwords over the network. + + + + +```java +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_PASSWORD_AUTH) + .build(); +Amplify.Auth.signIn( + "hello@example.com", + "password", + options, + result -> Log.i("AuthQuickStart", "Sign in succeeded with result " + result), + error -> Log.e("AuthQuickStart", "Failed to sign in", error) +); +``` + + + + +```kotlin +val options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_PASSWORD_AUTH) + .build() +Amplify.Auth.signIn( + "hello@example.com", + "password", + options, + { result -> + Log.i("AuthQuickstart", "Next step for sign in is ${result.nextStep}") + }, + { error -> + Log.e("AuthQuickstart", "Failed to sign in", error) + } +) +``` + + + + +```kotlin +try { + val options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_PASSWORD_AUTH) + .build() + val result = Amplify.Auth.signIn( + username = "hello@example.com", + password = "password", + options = options + ) + Log.i("AuthQuickstart", "Next step for sign in is ${result.nextStep}") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Sign in failed", error) +} +``` + + + + +```java +AuthSignInOptions options = AWSCognitoAuthSignInOptions.builder() + .authFlowType(AuthFlowType.USER_PASSWORD_AUTH) + .build(); +RxAmplify.Auth.signIn("hello@example.com", "password", options) + .subscribe( + result -> Log.i("AuthQuickstart", "Next step for sign in is " + result.getNextStep()), + error -> Log.e("AuthQuickstart", error.toString()) + ); +``` + + + + +### Set up auth backend + +In order to use the authentication flow `USER_PASSWORD_AUTH`, your Cognito app client has to be configured to allow it. Amplify Gen 2 enables SRP auth by default. To enable USER_PASSWORD_AUTH, you can update the `backend.ts` file with the following changes: + +```ts title="amplify/backend.ts" +import { defineBackend } from '@aws-amplify/backend' +import { auth } from './auth/resource' +import { data } from './data/resource' + +const backend = defineBackend({ + auth, + data, +}); + +// highlight-start +backend.auth.resources.cfnResources.cfnUserPoolClient.explicitAuthFlows = [ + "ALLOW_USER_PASSWORD_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_USER_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" +]; +// highlight-end +``` + +### Migrate users with Amazon Cognito + +Amazon Cognito provides a trigger to migrate users from your existing user directory seamlessly into Cognito. You achieve this by configuring your User Pool's "Migration" trigger which invokes a Lambda function whenever a user that does not already exist in the user pool authenticates, or resets their password. + +In short, the Lambda function will validate the user credentials against your existing user directory and return a response object containing the user attributes and status on success. An error message will be returned if an error occurs. There's documentation around [how to set up this migration flow](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-import-using-lambda.html) and more detailed instructions on [how the lambda should handle request and response objects](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-migrate-user.html#cognito-user-pools-lambda-trigger-syntax-user-migration). + +## CUSTOM_AUTH flow + +Amazon Cognito User Pools supports customizing the authentication flow to enable custom challenge types, in addition to a password in order to verify the identity of users. The custom authentication flow is a series of challenge and response cycles that can be customized to meet different requirements. These challenge types may include CAPTCHAs or dynamic challenge questions. + +To define your challenges for custom authentication flow, you need to implement three Lambda triggers for Amazon Cognito. + +The flow is initiated by calling `signIn` with `AWSCognitoAuthSignInOptions` configured with `AuthFlowType.CUSTOM_AUTH_WITH_SRP` OR `AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP`. + +Follow the instructions in [Custom Auth Sign In](/gen1/[platform]/build-a-backend/auth/sign-in-custom-flow/) to learn about how to integrate custom authentication flow in your application with the Auth APIs. + + + + For more information about working with Lambda Triggers for custom authentication challenges, please visit [Amazon Cognito Developer Documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). diff --git a/src/pages/[platform]/build-a-backend/auth/manage-users/manage-webauthn-credentials/index.mdx b/src/pages/[platform]/build-a-backend/auth/manage-users/manage-webauthn-credentials/index.mdx new file mode 100644 index 00000000000..af358d68c50 --- /dev/null +++ b/src/pages/[platform]/build-a-backend/auth/manage-users/manage-webauthn-credentials/index.mdx @@ -0,0 +1,441 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Manage WebAuthn credentials', + description: 'Learn how to manage WebAuthn credentials', + platforms: [ + 'android', + 'angular', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue' + ] +}; + +export function getStaticPaths() { + return getCustomStaticPath(meta.platforms); +} + +export function getStaticProps() { + return { + props: { + meta, + } + }; +} + + + + +WebAuthn registration and authentication are not currently supported on React Native, other passwordless features are fully supported. + + + + +Amplify Auth enables your users to associate, keep track of, and delete passkeys. + +## Associate WebAuthN credentials + +Note that users must be authenticated to register a passkey. That also means users cannot create a passkey during sign up; consequently, they must have at least one other first factor authentication mechanism associated with their account to use WebAuthn. + +You can associate a passkey using the following API: + + + +```ts +import { associateWebAuthnCredential} from 'aws-amplify/auth'; + +await associateWebAuthnCredential(); + +``` + + + + + + + +```java +Amplify.Auth.associateWebAuthnCredential( + activity, + () -> Log.i("AuthQuickstart", "Associated credential"), + error -> Log.e("AuthQuickstart", "Failed to register credential", error) +); +``` + + + + +```kotlin +Amplify.Auth.associateWebAuthnCredential( + activity, + { Log.i("AuthQuickstart", "Associated credential") }, + { Log.e("AuthQuickstart", "Failed to register credential", error) } +) +``` + + + + +```kotlin +try { + val result = Amplify.Auth.associateWebAuthnCredential(activity) + Log.i("AuthQuickstart", "Associated credential") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Failed to associate credential", error) +} +``` + + + + +```java +RxAmplify.Auth.associateWebAuthnCredential(activity) + .subscribe( + result -> Log.i("AuthQuickstart", "Associated credential"), + error -> Log.e("AuthQuickstart", "Failed to associate credential", error) + ); +``` + + + + +You must supply an `Activity` instance so that Amplify can display the PassKey UI in your Application's [Task](https://developer.android.com/guide/components/activities/tasks-and-back-stack). + + + + + + + +```swift +func associateWebAuthNCredentials() async { + do { + try await Amplify.Auth.associateWebAuthnCredential() + print("WebAuthn credential was associated") + } catch { + print("Associate WebAuthn Credential failed: \(error)") + } +} +``` + + + + +```swift +func associateWebAuthNCredentials() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.associateWebAuthnCredential() + }.sink { + print("Associate WebAuthn Credential failed: \($0)") + } + receiveValue: { _ in + print("WebAuthn credential was associated") + } +} +``` + + + + + + +The user will be prompted to register a passkey using their local authenticator. Amplify will then associate that passkey with Cognito. + +## List WebAuthN credentials + +You can list registered passkeys using the following API: + + + +```ts +import { listWebAuthnCredentials } from 'aws-amplify/auth'; + +const result = await listWebAuthnCredentials(); + +for (const credential of result.credentials) { + console.log(`Credential ID: ${credential.credentialId}`); + console.log(`Friendly Name: ${credential.friendlyCredentialName}`); + console.log(`Relying Party ID: ${credential.relyingPartyId}`); + console.log(`Created At: ${credential.createdAt}`); +} + +``` + + + + + + +```swift +func listWebAuthNCredentials() async { + do { + let result = try await Amplify.Auth.listWebAuthnCredentials( + options: .init(pageSize: 5)) + + for credential in result.credentials { + print("Credential ID: \(credential.credentialId)") + print("Created At: \(credential.createdAt)") + print("Relying Party Id: \(credential.relyingPartyId)") + if let friendlyName = credential.friendlyName { + print("Friendly name: \(friendlyName)") + } + } + + // Fetch the next page + if let nextToken = result.nextToken { + let nextResult = try await Amplify.Auth.listWebAuthnCredentials( + options: .init( + pageSize: 5, + nextToken: nextToken)) + } + } catch { + print("Associate WebAuthn Credential failed: \(error)") + } +} +``` + + + + +```swift +func listWebAuthNCredentials() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.listWebAuthnCredentials( + options: .init(pageSize: 5)) + }.sink { + print("List WebAuthn Credential failed: \($0)") + } + receiveValue: { result in + for credential in result.credentials { + print("Credential ID: \(credential.credentialId)") + print("Created At: \(credential.createdAt)") + print("Relying Party Id: \(credential.relyingPartyId)") + if let friendlyName = credential.friendlyName { + print("Friendly name: \(friendlyName)") + } + } + + if let nextToken = result.nextToken { + // Fetch the next page + } + } +} +``` + + + + + + + + + + +```java +Amplify.Auth.listWebAuthnCredentials( + result -> result.getCredentials().forEach(credential -> { + Log.i("AuthQuickstart", "Credential ID: " + credential.getCredentialId()); + Log.i("AuthQuickstart", "Friendly Name: " + credential.getFriendlyName()); + Log.i("AuthQuickstart", "Relying Party ID: " + credential.getRelyingPartyId()); + Log.i("AuthQuickstart", "Created At: " + credential.getCreatedAt()); + }), + error -> Log.e("AuthQuickstart", "Failed to list credentials", error) +); +``` + + + + +```kotlin +Amplify.Auth.listWebAuthnCredentials( + { result -> + result.credentials.forEach { credential -> + Log.i("AuthQuickstart", "Credential ID: ${credential.credentialId}") + Log.i("AuthQuickstart", "Friendly Name: ${credential.friendlyName}") + Log.i("AuthQuickstart", "Relying Party ID: ${credential.relyingPartyId}") + Log.i("AuthQuickstart", "Created At: ${credential.createdAt}") + } + }, + { error -> Log.e("AuthQuickstart", "Failed to list credentials", error) } +) +``` + + + + +```kotlin +try { + val result = Amplify.Auth.listWebAuthnCredentials() + result.credentials.forEach { credential -> + Log.i("AuthQuickstart", "Credential ID: ${credential.credentialId}") + Log.i("AuthQuickstart", "Friendly Name: ${credential.friendlyName}") + Log.i("AuthQuickstart", "Relying Party ID: ${credential.relyingPartyId}") + Log.i("AuthQuickstart", "Created At: ${credential.createdAt}") + } +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Failed to list credentials", error) +} +``` + + + + +```java +RxAmplify.Auth.listWebAuthnCredentials() + .subscribe( + result -> result.getCredentials().forEach(credential -> { + Log.i("AuthQuickstart", "Credential ID: " + credential.getCredentialId()); + Log.i("AuthQuickstart", "Friendly Name: " + credential.getFriendlyName()); + Log.i("AuthQuickstart", "Relying Party ID: " + credential.getRelyingPartyId()); + Log.i("AuthQuickstart", "Created At: " + credential.getCreatedAt()); + }), + error -> Log.e("AuthQuickstart", "Failed to list credentials", error) + ); +``` + + + + + +## Delete WebAuthN credentials + +You can delete a passkey with the following API: + + + +```ts +import { deleteWebAuthnCredential } from 'aws-amplify/auth'; + +const id = "credential-id-to-delete"; + +await deleteWebAuthnCredential({ + credentialId: id +}); +``` + + + + + + +```swift +func deleteWebAuthNCredentials(credentialId: String) async { + do { + try await Amplify.Auth.deleteWebAuthnCredential(credentialId: credentialId) + print("WebAuthn credential was deleted") + } catch { + print("Delete WebAuthn Credential failed: \(error)") + } +} +``` + + + + +```swift +func deleteWebAuthNCredentials(credentialId: String) -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.deleteWebAuthnCredential(credentialId: credentialId) + }.sink { + print("Delete WebAuthn Credential failed: \($0)") + } + receiveValue: { _ in + print("WebAuthn credential was deleted") + } +} +``` + + + + + + + + + +```java +Amplify.Auth.deleteWebAuthnCredential( + credentialId, + (result) -> Log.i("AuthQuickstart", "Deleted credential"), + error -> Log.e("AuthQuickstart", "Failed to delete credential", error) +); +``` + + + + +```kotlin +Amplify.Auth.deleteWebAuthnCredential( + credentialId, + { Log.i("AuthQuickstart", "Deleted credential") }, + { Log.e("AuthQuickstart", "Failed to delete credential", error) } +) +``` + + + + +```kotlin +try { + val result = Amplify.Auth.deleteWebAuthnCredential(credentialId) + Log.i("AuthQuickstart", "Deleted credential") +} catch (error: AuthException) { + Log.e("AuthQuickstart", "Failed to delete credential", error) +} +``` + + + + +```java +RxAmplify.Auth.deleteWebAuthnCredential(credentialId) + .subscribe( + result -> Log.i("AuthQuickstart", "Deleted credential"), + error -> Log.e("AuthQuickstart", "Failed to delete credential", error) + ); +``` + + + + +The delete passkey API has only the required `credentialId` as input, and it does not return a value. + + + + + +## Practical example + +Here is a code example that uses the list and delete APIs together. In this example, the user has 3 passkeys registered. They want to list all passkeys while using a `pageSize` of 2 as well as delete the first passkey in the list. + +```ts +import { + listWebAuthnCredentials, + deleteWebAuthnCredential +} from 'aws-amplify/auth'; + +let passkeys = []; + +const result = await listWebAuthnCredentials({ pageSize: 2 }); + +passkeys.push(...result.credentials); + +const nextPage = await listWebAuthnCredentials({ + pageSize: 2, + nextToken: result.nextToken, +}); + +passkeys.push(...nextPage.credentials); + +const id = passkeys[0].credentialId; + +await deleteWebAuthnCredential({ + credentialId: id +}); +``` + + diff --git a/src/pages/[platform]/build-a-backend/auth/modify-resources-with-cdk/index.mdx b/src/pages/[platform]/build-a-backend/auth/modify-resources-with-cdk/index.mdx index 3d5c3b8b143..df7143cec42 100644 --- a/src/pages/[platform]/build-a-backend/auth/modify-resources-with-cdk/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/modify-resources-with-cdk/index.mdx @@ -58,6 +58,7 @@ cfnUserPool.policies = { ``` ## Override Cognito UserPool multi-factor authentication options + While Email MFA is not yet supported with `defineAuth`, this feature can be enabled by modifying the underlying CDK construct. Start by ensuring your `defineAuth` resource configuration includes a compatible account recovery option and a custom SES sender. @@ -89,6 +90,7 @@ export const auth = defineAuth({ }, }) ``` + Next, extend the underlying CDK construct by activating [Amazon Cognito's Advanced Security Features (ASF)](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-advanced-security.html) and add `EMAIL_OTP` to the enabled MFA options. ```ts title="amplify/backend.ts" @@ -114,3 +116,36 @@ cfnUserPool.enabledMfas = [...(cfnUserPool.enabledMfas || []), "EMAIL_OTP"] {/* token validity */} {/* BYO custom idp construct */} {/* extend auth/unauth roles */} + + + +### Override Cognito UserPool to enable passwordless sign-in methods + +You can modify the underlying Cognito user pool resource to enable sign in with passwordless methods. [Learn more about passwordless sign-in methods](/[platform]/build-a-backend/auth/concepts/passwordless/). + +```ts title="amplify/backend.ts" +import { defineBackend } from "@aws-amplify/backend" +import { auth } from "./auth/resource" + +const backend = defineBackend({ + auth, +}) + +const { cfnResources } = backend.auth.resources; +const { cfnUserPool, cfnUserPoolClient } = cfnResources; + +cfnUserPool.addPropertyOverride( + 'Policies.SignInPolicy.AllowedFirstAuthFactors', + ['PASSWORD', 'WEB_AUTHN', 'EMAIL_OTP', 'SMS_OTP'] +); + +cfnUserPoolClient.explicitAuthFlows = [ + 'ALLOW_REFRESH_TOKEN_AUTH', + 'ALLOW_USER_AUTH' +]; + +/* Needed for WebAuthn */ +cfnUserPool.addPropertyOverride('WebAuthnRelyingPartyID', ''); +cfnUserPool.addPropertyOverride('WebAuthnUserVerification', 'preferred'); +``` + diff --git a/src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx b/src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx index 3fae274bd6d..0b14f810977 100644 --- a/src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx @@ -287,7 +287,7 @@ npm add \ aws-amplify \ @react-native-community/netinfo \ @react-native-async-storage/async-storage \ - react-native-safe-area-context \ + react-native-safe-area-context@^4.2.5 \ react-native-get-random-values ``` @@ -316,7 +316,7 @@ Next, update the `App.tsx` file with the following to set up the authentication ```typescript import React from "react"; -import { Button, View, StyleSheet, SafeAreaView } from "react-native"; +import { Button, View, StyleSheet } from "react-native"; import { Amplify } from "aws-amplify"; import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react-native"; import outputs from "./amplify_outputs.json"; @@ -337,9 +337,7 @@ const App = () => { return ( - - - + ); diff --git a/src/pages/[platform]/build-a-backend/auth/use-existing-cognito-resources/index.mdx b/src/pages/[platform]/build-a-backend/auth/use-existing-cognito-resources/index.mdx index ceb0aa498c6..4902c716ee2 100644 --- a/src/pages/[platform]/build-a-backend/auth/use-existing-cognito-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/use-existing-cognito-resources/index.mdx @@ -150,6 +150,24 @@ export const auth = referenceAuth({ }); ``` +Additionally, you can also use the `groups` property to reference groups in your user pool. This is useful if you want to work with groups in your application and provide access to resources such as storage based on group membership. + +```ts title="amplify/auth/resource.ts" +import { referenceAuth } from '@aws-amplify/backend'; +import { getUser } from "../functions/get-user/resource"; + +export const auth = referenceAuth({ + userPoolId: 'us-east-1_xxxx', + identityPoolId: 'us-east-1:b57b7c3b-9c95-43e4-9266-xxxx', + authRoleArn: 'arn:aws:iam::xxxx:role/amplify-xxxx-mai-amplifyAuthauthenticatedU-xxxx', + unauthRoleArn: 'arn:aws:iam::xxxx:role/amplify-xxxx-mai-amplifyAuthunauthenticate-xxxx', + userPoolClientId: 'xxxx', + groups: { + admin: "arn:aws:iam::xxxx:role/amplify-xxxx-mai-amplifyAuthadminGroupRole-xxxx", + }, +}); +``` + In a team setting you may want to reference a different set of auth resources depending on the deployment context. For instance if you have a `staging` branch that should reuse resources from a separate "staging" environment compared to a `production` branch that should reuse resources from the separate "production" environment. In this case we recommend using environment variables. ```ts title="amplify/auth/resource.ts" diff --git a/src/pages/[platform]/build-a-backend/data/aws-appsync-apollo-extensions/index.mdx b/src/pages/[platform]/build-a-backend/data/aws-appsync-apollo-extensions/index.mdx index b7068f5ad97..d12f1f7d0a5 100644 --- a/src/pages/[platform]/build-a-backend/data/aws-appsync-apollo-extensions/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/aws-appsync-apollo-extensions/index.mdx @@ -23,15 +23,13 @@ export function getStaticProps(context) { }; } -AWS AppSync Apollo Extensions provide a seamless way to connect to your AWS AppSync backend using Apollo client, an open-source GraphQL client. - +AWS AppSync Apollo Extensions provide a seamless way to connect to your AWS AppSync backend using Apollo client, an open-source GraphQL client. To learn more about Apollo, see https://www.apollographql.com/docs/ios/. - To learn more about Apollo, see https://www.apollographql.com/docs/kotlin. @@ -42,22 +40,26 @@ To learn more about Apollo, see https://www.apollographql.com/docs/kotlin. AWS AppSync Apollo Extensions provide AWS AppSync authorizers to be used with the Apollo client to make it simple to apply the correct authorization payloads to your GraphQL operations. -The Amplify library provides components to facilitate configuring the authorizers with Apollo client by providing configuration values to connect to your Amplify Data backend. + +Additionally, we publish an optional Amplify extension that allows Amplify to provide auth tokens and signing logic for the corresponding Authorizers. + + + +Additionally, the included Amplify components allow Amplify to provide auth tokens and signing logic for the corresponding Authorizers. + -### Install the AWS AppSync Apollo Extensions library +## Install the AWS AppSync Apollo Extensions library -To connect Apollo to AppSync without using Amplify, add the `apollo-appsync` dependency to your app/build.gradle.kts file. If your -application is using Amplify Android, add the `apollo-appsync-amplify` dependency instead. + + + + +Add the `apollo-appsync-amplify` dependency to your app/build.gradle.kts file. ```kotlin title="app/build.gradle.kts" dependencies { - // highlight-start - // Connect Apollo to AppSync without using Amplify - implementation("com.amplifyframework:apollo-appsync:1.0.0") - // highlight-end - // or // highlight-start // Connect Apollo to AppSync, delegating some implementation details to Amplify implementation("com.amplifyframework:apollo-appsync-amplify:1.0.0") @@ -65,6 +67,24 @@ dependencies { } ``` + + + + +Add the `apollo-appsync` dependency to your app/build.gradle.kts file. + +```kotlin title="app/build.gradle.kts" +dependencies { + // highlight-start + // Connect Apollo to AppSync without using Amplify + implementation("com.amplifyframework:apollo-appsync:1.0.0") + // highlight-end +} +``` + + + + @@ -78,29 +98,71 @@ Enter its GitHub URL (`https://github.com/aws-amplify/aws-appsync-apollo-extensi -### Connecting to AWS AppSync with Apollo client +## Connecting to AWS AppSync with Apollo client -AWS AppSync supports the following [authorization modes](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html): + -#### API_KEY +### Creating the ApolloClient - + -```swift -import AWSAppSyncApolloExtensions + +Before you begin, you will need an Amplify Data backend deploy. To get started, see [Set up Data](/[platform]/build-a-backend/data/set-up-data/). -let authorizer = APIKeyAuthorizer(apiKey: "[API_KEY]") -let interceptor = AppSyncInterceptor(authorizer) +Once you have deployed your backend and created the `amplify_outputs.json` file, you can use Amplify library to read and retrieve your configuration values with the following steps: + +```kotlin +// Use apiKey auth mode, reading configuration from AmplifyOutputs +val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) +val apolloClient = ApolloClient.Builder() + .appSync(connector.endpoint, connector.apiKeyAuthorizer()) + .build() ``` + - + +You can create your Apollo client by using our provided AWS AppSync endpoint and authorizer classes. - +```kotlin +val endpoint = AppSyncEndpoint("") +// Continue Reading to see more authorizer examples +val authorizer = ApiKeyAuthorizer("[API_KEY]") +val apolloClient = ApolloClient.Builder() + .appSync(endpoint, authorizer) + .build() +``` + + + + +### Providing AppSync Authorizers + + + + -An `ApiKeyAuthorizer` can be used with a hardcoded API key, by fetching the key from some source, or reading it from `amplify_outputs.json`: +The AWS AppSync Apollo Extensions library provides a number of Authorizer classes to match the various authorization strategies that may be in use in your schema. You should choose the appropriate Authorizer type for your authorization strategy. To read more about the strategies and their corresponding auth modes, see [Available authorization strategies](/[platform]/build-a-backend/data/customize-authz/#available-authorization-strategies). + +Some common ones are + +* `publicAPIkey` strategy, `apiKey` authMode, **APIKeyAuthorizer** +* `guest` strategy, `identityPool` authMode, **IAMAuthorizer** +* `owner` strategy, `userPool` authMode, **AuthTokenAuthorizer** + +If you define multiple authorization strategies within your schema, you will have to create separate Apollo client instances for each Authorizer that you want to use in your app. + +#### API_KEY + +An `ApiKeyAuthorizer` can read the API key from `amplify_outputs.json`, provide a hardcoded API key, or fetch the API key from some source: ```kotlin // highlight-start +// Using ApolloAmplifyConnector to read API key from amplify_outputs.json +val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) +val authorizer = connector.apiKeyAuthorizer() +//highlight-end +// or +// highlight-start // Use a hard-coded API key val authorizer = ApiKeyAuthorizer("[API_KEY]") //highlight-end @@ -110,51 +172,10 @@ val authorizer = ApiKeyAuthorizer("[API_KEY]") // so it should implement appropriate caching internally. val authorizer = ApiKeyAuthorizer { fetchApiKey() } //highlight-end -// or -// highlight-start -// Using ApolloAmplifyConnector to read API key from amplify_outputs.json -val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) -val authorizer = connector.apiKeyAuthorizer() -//highlight-end ``` - - #### AMAZON_COGNITO_USER_POOLS - - -If you are using Amplify Auth, you can create a method that retrieves the Cognito access token - -```swift -import Amplify - -func getUserPoolAccessToken() async throws -> String { - let authSession = try await Amplify.Auth.fetchAuthSession() - if let result = (authSession as? AuthCognitoTokensProvider)?.getCognitoTokens() { - switch result { - case .success(let tokens): - return tokens.accessToken - case .failure(let error): - throw error - } - } - throw AuthError.unknown("Did not receive a valid response from fetchAuthSession for get token.") -} -``` - -Then create the AuthTokenAuthorizer with this method. - -```swift -import AWSAppSyncApolloExtensions - -let authorizer = AuthTokenAuthorizer(fetchLatestAuthToken: getUserPoolAccessToken) -let interceptor = AppSyncInterceptor(authorizer) -``` - - - - You can use `AmplifyApolloConnector` to get an `AuthTokenAuthorizer` instance that supplies the token for the current logged-in Amplify user, or implement the token fetching yourself. ```kotlin @@ -181,30 +202,11 @@ val authorizer = AuthTokenAuthorizer { //highlight-end ``` - - You can provide your own custom `fetchLatestAuthToken` provider for **AWS_LAMBDA** and **OPENID_CONNECT** auth modes. #### AWS_IAM - - -If you are using Amplify Auth, you can use the following method for AWS_IAM auth - -```swift -import AWSCognitoAuthPlugin -import AWSAppSyncApolloExtensions - -let authorizer = IAMAuthorizer( - signRequest: AWSCognitoAuthPlugin.createAppSyncSigner( - region: "[REGION]")) -``` - - - - - -If you are using `apollo-appsync-amplify`, you can use the `ApolloAmplifyConnector` to delegate token fetching and request +You can use the `ApolloAmplifyConnector` to delegate token fetching and request signing to Amplify. ```kotlin @@ -223,15 +225,131 @@ val authorizer = IamAuthorizer { //highlight-end ``` + + + + +AWS AppSync supports the following [authorization modes](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html). Use the corresponding Authorizer that matches the chosen authorization type. + +Some common ones are + +* API Key Authorization -> **APIKeyAuthorizer** +* IAM Authorization -> **IAMAuthorizer** +* Cognito User Pools -> **AuthTokenAuthorizer** + +If you apply multiple authorization directives in your schema, you will have to create separate Apollo client instances for each Authorizer that you want to use in your app. + +#### API_KEY + +An `ApiKeyAuthorizer` can be used with a hardcoded API key or by fetching the key from some source.: + +```kotlin +// highlight-start +// Use a hard-coded API key +val authorizer = ApiKeyAuthorizer("[API_KEY]") +//highlight-end +// or +// highlight-start +// Fetch the API key from some source. This function may be called many times, +// so it should implement appropriate caching internally. +val authorizer = ApiKeyAuthorizer { fetchApiKey() } +//highlight-end +``` + +#### AMAZON_COGNITO_USER_POOLS + +When working directly with AppSync, you must implement the token fetching yourself. + +```kotlin +// highlight-start +// Use your own token fetching. This function may be called many times, +// so it should implement appropriate caching internally. +val authorizer = AuthTokenAuthorizer { + fetchLatestAuthToken() +} +//highlight-end +``` + +#### AWS_IAM + +When working directly with AppSync, you must implement the request signing yourself. + +```kotlin +// highlight-start +// Provide an implementation of the signing function. This function should implement the +// AWS Sig-v4 signing logic and return the authorization headers containing the token and signature. +val authorizer = IamAuthorizer { signRequestAndReturnHeaders(it) } +// highlight-end +``` + + + + -### Connecting Amplify Data to Apollo client -Before you begin, you will need an Amplify Data backend deploy. To get started, see [Set up Data](/[platform]/build-a-backend/data/set-up-data/). + +AWS AppSync supports the following [authorization modes](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html): -Once you have deployed your backend and created the `amplify_outputs.json` file, you can use Amplify library to read and retrieve your configuration values with the following steps: +### API_KEY + +```swift +import AWSAppSyncApolloExtensions + +let authorizer = APIKeyAuthorizer(apiKey: "[API_KEY]") +let interceptor = AppSyncInterceptor(authorizer) +``` + +### AMAZON_COGNITO_USER_POOLS + +If you are using Amplify Auth, you can create a method that retrieves the Cognito access token + +```swift +import Amplify + +func getUserPoolAccessToken() async throws -> String { + let authSession = try await Amplify.Auth.fetchAuthSession() + if let result = (authSession as? AuthCognitoTokensProvider)?.getCognitoTokens() { + switch result { + case .success(let tokens): + return tokens.accessToken + case .failure(let error): + throw error + } + } + throw AuthError.unknown("Did not receive a valid response from fetchAuthSession for get token.") +} +``` + +Then create the AuthTokenAuthorizer with this method. + +```swift +import AWSAppSyncApolloExtensions + +let authorizer = AuthTokenAuthorizer(fetchLatestAuthToken: getUserPoolAccessToken) +let interceptor = AppSyncInterceptor(authorizer) +``` + +### AWS_IAM + +If you are using Amplify Auth, you can use the following method for AWS_IAM auth + +```swift +import AWSCognitoAuthPlugin +import AWSAppSyncApolloExtensions + +let authorizer = IAMAuthorizer( + signRequest: AWSCognitoAuthPlugin.createAppSyncSigner( + region: "[REGION]")) +``` + +## Connecting Amplify Data to Apollo client + +Before you begin, you will need an Amplify Data backend deploy. To get started, see [Set up Data](/[platform]/build-a-backend/data/set-up-data/). + +Once you have deployed your backend and created the `amplify_outputs.json` file, you can use Amplify library to read and retrieve your configuration values with the following steps: 1. Enter its GitHub URL (`https://github.com/aws-amplify/amplify-swift`), select **Up to Next Major Version** and click **Add Package** 2. Select the following libraries: @@ -266,32 +384,7 @@ func createApolloClient() throws -> ApolloClient { } ``` - - - - -Add `apollo-appsync-amplify` dependency to your app/build.gradle.kts file: - -```kotlin title="app/build.gradle.kts" -dependencies { - // highlight-start - implementation("com.amplifyframework:apollo-appsync-amplify:1.0.0") - // highlight-end -} -``` - -```kotlin -// Use apiKey auth mode, reading configuration from AmplifyOutputs -val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) -val apolloClient = ApolloClient.Builder() - .appSync(connector.endpoint, connector.apiKeyAuthorizer()) - .build() -``` - - - -Depending on your authorization strategy defined on your schema, you can use the corresponding Authorizer. To read more about the strategies and their corresponding auth modes, see [Available authorization strategies](/[platform]/build-a-backend/data/customize-authz/#available-authorization-strategies). - +The AWS AppSync Apollo Extensions library provides a number of Authorizer classes to match the various authorization strategies that may be in use in your schema. You should choose the appropriate Authorizer type for your authorization strategy. To read more about the strategies and their corresponding auth modes, see [Available authorization strategies](/[platform]/build-a-backend/data/customize-authz/#available-authorization-strategies). Some common ones are @@ -299,16 +392,18 @@ Some common ones are * `guest` strategy, `identityPool` authMode, **IAMAuthorizer** * `owner` strategy, `userPool` authMode, **AuthTokenAuthorizer** -If you define multiple authorization strategies on a single model, you will have to create separate Apollo client instances for each Authorizer that you want to use in your app. +If you define multiple authorization strategies within your schema, you will have to create separate Apollo client instances for each Authorizer that you want to use in your app. + + -### Downloading the AWS AppSync schema +## Downloading the AWS AppSync schema The schema is used by Apollo’s code generation tool to generate API code that helps you execute GraphQL operations. The following steps integrate your AppSync schema with Apollo's code generation process: 1. Navigate to your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) 2. On the left side, select Schema -3. When viewing your schema, there should a “Export schema” drop down. Select this and download the `schema.json` file. +3. Select the "Export schema" dropdown and download the `schema.json` file. 4. Add this file to your project as directed by [Apollo Code Generation documentation](https://www.apollographql.com/docs/ios/code-generation/introduction). You can alternatively download the introspection schema using the [`fetch-schema`](https://www.apollographql.com/docs/ios/code-generation/codegen-cli#fetch-schema) command with the `amplify-ios-cli` tool. @@ -316,25 +411,77 @@ You can alternatively download the introspection schema using the [`fetch-schema + 1. Navigate to your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) 2. On the left side, select Schema -3. When viewing your schema, there should a “Export schema” drop down. Select this and download the `schema.json` file. +3. Select the "Export schema" dropdown and download the `schema.json` file. 4. Add this file to your project as directed by [Apollo documentation](https://www.apollographql.com/docs/kotlin/advanced/plugin-recipes#specifying-the-schema-location) + -### Performing Queries, Mutations, and Subscriptions with Apollo client +## Generating Queries, Mutations, and Subscriptions for Apollo client + + + + + + +**Amplify provided .graphql files** +1. Within your Amplify Gen 2 backend, run: `npx ampx generate graphql-client-code --format graphql-codegen --statement-target graphql --out graphql` +2. Copy the generated files (`mutations.graphql`, `queries.graphql`, `subscriptions.graphql`) to your `{app}/src/main/graphql` folder as shown in the [Apollo documentation](https://www.apollographql.com/docs/kotlin#getting-started) + +**Manual** +1. Navigate to the **Queries** tab in your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home). Here, you can test queries, mutations, and subscriptions in the GraphQL playground. +2. Enter your GraphQL operation (query, mutation, or subscription) in the editor and select **Run** to execute it. +3. Observe the request and response structure in the results. This gives you insight into the exact call patterns and structure that Apollo will use. +4. Copy the GraphQL operation(s) from the playground and pass them to to your `{app}/src/main/graphql` folder as shown in the [Apollo documentation](https://www.apollographql.com/docs/kotlin#getting-started) + + + + 1. Navigate to the **Queries** tab in your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home). Here, you can test queries, mutations, and subscriptions in the GraphQL playground. 2. Enter your GraphQL operation (query, mutation, or subscription) in the editor and click **Run** to execute it. 3. Observe the request and response structure in the results. This gives you insight into the exact call patterns and structure that Apollo will use. 4. Copy the GraphQL operation from the playground and pass it to Apollo's code generation tool to automatically generate the corresponding API code for your project. -## Connecting to AWS AppSync real-time endpoint + -The following example shows how you can create an Apollo client that allows performing GraphQL subscription operations with AWS AppSync. + + + +1. Navigate to the **Queries** tab in your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home). Here, you can test queries, mutations, and subscriptions in the GraphQL playground. +2. Enter your GraphQL operation (query, mutation, or subscription) in the editor and click **Run** to execute it. +3. Observe the request and response structure in the results. This gives you insight into the exact call patterns and structure that Apollo will use. +4. Copy the GraphQL operation from the playground and pass it to Apollo's code generation tool to automatically generate the corresponding API code for your project. + + + + + +## Type Mapping AppSync Scalars +By default, [AWS AppSync Scalars](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#graph-ql-aws-appsync-scalars) will default to the `Any` type. You can map these scalars to more explicit types by editing the `apollo` block in your `app/build.gradle[.kts]` file. In the example below, we are now mapping a few of our AppSync scalar types to `String` instead of `Any`. Additional improvements could be made by writing [custom class adapters](https://www.apollographql.com/docs/kotlin/essentials/custom-scalars#define-class-mapping) to convert date/time scalars into Kotlin date/time class types. + +```kotlin +apollo { + service("{serviceName}") { + packageName.set("{packageName}") + mapScalarToKotlinString("AWSDateTime") + mapScalarToKotlinString("AWSEmail") + } +} +``` + + + + +## Connecting to AWS AppSync real-time endpoint + +The following example shows how you can create an Apollo client that allows performing GraphQL subscription operations with AWS AppSync. + ```swift import Apollo import ApolloAPI @@ -371,28 +518,3 @@ func createApolloClient() throws -> ApolloClient { ``` - - - -When using `apollo-appsync`, you create `AppSyncEndpoint` and `AppSyncAuthorizer` instances, and pass them to the ApolloClient's Builder extension function. - -```kotlin -val endpoint = AppSyncEndpoint("") -val authorizer = /* your Authorizer */ - -val apolloClient = ApolloClient.Builder() - .appSync(endpoint, authorizer) - .build() -``` - -When using `apollo-appsync-amplify`, you can get the endpoint and authorizer from an `ApolloAmplifyConnector` to connect to your Amplify backend. - -```kotlin -val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) - -val apolloClient = ApolloClient.Builder() - .appSync(connector.endpoint, connector.apiKeyAuthorizer()) // or .authTokenAuthorizer(), or .iamAuthorizer() - .build() -``` - - diff --git a/src/pages/[platform]/build-a-backend/data/connect-event-api/index.mdx b/src/pages/[platform]/build-a-backend/data/connect-event-api/index.mdx index 777d84dc8c5..3f79f58a3bc 100644 --- a/src/pages/[platform]/build-a-backend/data/connect-event-api/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/connect-event-api/index.mdx @@ -89,8 +89,8 @@ export default function App() { <>
    - {myEvents.map((event, index) => ( -
  • {JSON.stringify(event)}
  • + {myEvents.map((data) => ( +
  • {JSON.stringify(data.event)}
  • ))}
@@ -98,6 +98,170 @@ export default function App() { } ``` -## Connect to an Event API with an existing Amplify backend +## Add an Event API to an existing Amplify backend -Coming Soon +This guide walks through how you can add an Event API to an existing Amplify backend. We'll be using Cognito User Pools for authenticating with Event API from our frontend application. Any signed in user will be able to subscribe to the Event API and publish events. + +Before you begin, you will need: + +- An existing Amplify backend (see [Quickstart](/[platform]/start/quickstart/)) +- Latest versions of `@aws-amplify/backend` and `@aws-amplify/backend-cli` (`npm add @aws-amplify/backend@latest @aws-amplify/backend-cli@latest`) + +### Update Backend Definition + +First, we'll add a new Event API to our backend definition. + +```ts title="amplify/backend.ts" +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +// highlight-start +// import CDK resources: +import { + CfnApi, + CfnChannelNamespace, + AuthorizationType, +} from 'aws-cdk-lib/aws-appsync'; +import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +// highlight-end + +const backend = defineBackend({ + auth, +}); + +// highlight-start +// create a new stack for our Event API resources: +const customResources = backend.createStack('custom-resources'); + +// add a new Event API to the stack: +const cfnEventAPI = new CfnApi(customResources, 'CfnEventAPI', { + name: 'my-event-api', + eventConfig: { + authProviders: [ + { + authType: AuthorizationType.USER_POOL, + cognitoConfig: { + awsRegion: customResources.region, + // configure Event API to use the Cognito User Pool provisioned by Amplify: + userPoolId: backend.auth.resources.userPool.userPoolId, + }, + }, + ], + // configure the User Pool as the auth provider for Connect, Publish, and Subscribe operations: + connectionAuthModes: [{ authType: AuthorizationType.USER_POOL }], + defaultPublishAuthModes: [{ authType: AuthorizationType.USER_POOL }], + defaultSubscribeAuthModes: [{ authType: AuthorizationType.USER_POOL }], + }, +}); + +// create a default namespace for our Event API: +const namespace = new CfnChannelNamespace( + customResources, + 'CfnEventAPINamespace', + { + apiId: cfnEventAPI.attrApiId, + name: 'default', + } +); + +// attach a policy to the authenticated user role in our User Pool to grant access to the Event API: +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy( + new Policy(customResources, 'AppSyncEventPolicy', { + statements: [ + new PolicyStatement({ + actions: [ + 'appsync:EventConnect', + 'appsync:EventSubscribe', + 'appsync:EventPublish', + ], + resources: [`${cfnEventAPI.attrApiArn}/*`, `${cfnEventAPI.attrApiArn}`], + }), + ], + }) +); + +// finally, add the Event API configuration to amplify_outputs: +backend.addOutput({ + custom: { + events: { + url: `https://${cfnEventAPI.getAtt('Dns.Http').toString()}/event`, + aws_region: customResources.region, + default_authorization_type: AuthorizationType.USER_POOL, + }, + }, +}); +// highlight-end +``` + +### Deploy Backend + +To test your changes, deploy your Amplify Sandbox. + +```bash title="Terminal" showLineNumbers={false} +npx ampx sandbox +``` + +### Connect your frontend application + +After the sandbox deploys, connect your frontend application to the Event API. We'll be using the [Amplify Authenticator component](https://ui.docs.amplify.aws/react/connected-components/authenticator) to sign in to our Cognito User Pool. + +If you don't already have the Authenticator installed, you can install it by running `npm add @aws-amplify/ui-react`. + +```tsx title="src/App.tsx" +import { useEffect, useState } from 'react'; +import { Amplify } from 'aws-amplify'; +import { events, type EventsChannel } from 'aws-amplify/data'; +import { Authenticator } from '@aws-amplify/ui-react'; +import '@aws-amplify/ui-react/styles.css'; +import outputs from '../amplify_outputs.json'; + +Amplify.configure(outputs); + +export default function App() { + const [myEvents, setMyEvents] = useState[]>([]); + + useEffect(() => { + let channel: EventsChannel; + + const connectAndSubscribe = async () => { + channel = await events.connect('default/channel'); + + channel.subscribe({ + next: (data) => { + console.log('received', data); + setMyEvents((prev) => [data, ...prev]); + }, + error: (err) => console.error('error', err), + }); + }; + + connectAndSubscribe(); + + return () => channel && channel.close(); + }, []); + + async function publishEvent() { + await events.post('default/channel', { some: 'data' }); + } + + return ( + + {({ signOut, user }) => ( + <> +
+

Welcome, {user.username}

+ +
+
+ +
    + {myEvents.map((data) => ( +
  • {JSON.stringify(data.event)}
  • + ))} +
+
+ + )} +
+ ); +} +``` diff --git a/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx b/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx index 0427a514f03..aae775a5366 100644 --- a/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/connect-to-API/index.mdx @@ -694,3 +694,42 @@ await Amplify.addPlugins([ ``` + + + +## Use an additional Data endpoint + +If you have an additional Data endpoint that you're managing with a different Amplify project or through other means, this section will show you how to utilize that endpoint in your frontend code. + +This is done by specifying the `endpoint` parameter on the `generateClient` function. + +```ts +import { generateClient } from 'aws-amplify/data'; + +const client = generateClient({ + endpoint: 'https://my-other-endpoint.com/graphql', +}); +``` + +If this Data endpoint shares its authorization configuration (for example, both endpoints share the same user pool and/or identity pool as the one in your `amplify_outputs.json` file), you can specify the `authMode` parameter on `generateClient`. + +```ts +const client = generateClient({ + endpoint: 'https://my-other-endpoint.com/graphql', + authMode: 'userPool', +}); +``` + +If the endpoint uses API Key authorization, you can pass in the `apiKey` parameter on `generateClient`. + +```ts +const client = generateClient({ + endpoint: 'https://my-other-endpoint.com/graphql', + authMode: 'apiKey', + apiKey: 'my-api-key', +}); +``` + +If the endpoint uses a different authorization configuration, you can manually pass in the authorization header using the instructions in the [Set custom request headers](#set-custom-request-headers) section. + + diff --git a/src/pages/[platform]/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/index.mdx b/src/pages/[platform]/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/index.mdx index b919d0e4a35..1efa65df3f9 100644 --- a/src/pages/[platform]/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/index.mdx @@ -208,7 +208,7 @@ When deploying your app to production, you need to [add the database connection ## Rename generated models and fields -To improve the ergonomics of your API, you might want to rename the generate fields or types to better accommodate your use case. Use the `renameModels()` and `renameModelFields()` modifiers to rename the auto-inferred data models and their fields. +To improve the ergonomics of your API, you might want to rename the generate fields or types to better accommodate your use case. Use the `renameModels()` modifier to rename the auto-inferred data models. ```ts // Rename models or fields to be more idiomatic for frontend code diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/batch-ddb-operations/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/batch-ddb-operations/index.mdx new file mode 100644 index 00000000000..7a1ed30efc5 --- /dev/null +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/batch-ddb-operations/index.mdx @@ -0,0 +1,153 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Batch DynamoDB Operations', + description: + 'Batch DynamoDB Operations', + platforms: [ + 'android', + 'angular', + 'flutter', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps() { + return { + props: { + meta + } + }; +} + +Batch DynamoDB operations allow you to add multiple items in single mutation. + +## Step 1 - Define a custom mutation + +```ts title="amplify/data/resource.ts" +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; + +const schema = a.schema({ + // 1. Define your return type as a custom type or model + Post: a.model({ + id: a.id(), + content: a.string(), + likes: a.integer() + }), + + // 2. Define your mutation with the return type and, optionally, arguments + BatchCreatePost: a + .mutation() + // arguments that this query accepts + .arguments({ + content: a.string().array() + }) + .returns(a.ref('Post').array()) + // only allow signed-in users to call this API + .authorization(allow => [allow.authenticated()]) +}); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema +}); +``` + +## Step 2 - Configure custom business logic handler code + +After your query or mutation is defined, you need to author your custom business logic using a [custom resolver powered by AppSync JavaScript resolver](https://docs.aws.amazon.com/appsync/latest/devguide/tutorials-js.html). + +Custom resolvers work on a "request/response" basis. You choose a data source, map your request to the data source's input parameters, and then map the data source's response back to the query/mutation's return type. Custom resolvers provide the benefit of no cold starts, less infrastructure to manage, and no additional charge for Lambda function invocations. Review [Choosing between custom resolver and function](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#choosing-data-source). + +In your `amplify/data/resource.ts` file, define a custom handler using `a.handler.custom`. + +```ts title="amplify/data/resource.ts" +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; + +const schema = a.schema({ + Post: a.model({ + id: a.id(), + content: a.string(), + likes: a.integer() + }), + + BatchCreatePost: a + .mutation() + .arguments({ + contents: a.string().array() + }) + .returns(a.ref('Post').array()) + .authorization(allow => [allow.authenticated()]) + // 1. Add the custom handler + .handler( + a.handler.custom({ + dataSource: a.ref('Post'), + entry: './BatchCreatePostHandler.js', + }) + ) +}); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema +}); +``` + +Amplify will store some values in the resolver context stash that can be accessed in the custom resolver. + +| Name | Description | +| ------------------------- | -------------------------------------------- | +| awsAppsyncApiId | The ID of the AppSync API. | +| amplifyApiEnvironmentName | The Amplify api environment name. (`NONE` in sandbox) | + +The Amplify generated DynamoDB table names can be constructed from the variables in the context stash. The table name is in the format `--`. For example, the table name for the `Post` model would be `Post-123456-dev` where `123456` is the AppSync API ID and `dev` is the Amplify API environment name. + +```ts title="amplify/data/BatchCreatePostHandler.js" +import { util } from '@aws-appsync/utils'; + +export function request(ctx) { + var now = util.time.nowISO8601(); + + return { + operation: 'BatchPutItem', + tables: { + [`Post-${ctx.stash.awsAppsyncApiId}-${ctx.stash.amplifyApiEnvironmentName}`]: ctx.args.contents.map((content) => + util.dynamodb.toMapValues({ + content, + id: util.autoId(), + createdAt: now, + updatedAt: now, + }) + ), + }, + }; +} + +export function response(ctx) { + if (ctx.error) { + util.error(ctx.error.message, ctx.error.type); + } + return ctx.result.data[`Post-${ctx.stash.awsAppsyncApiId}-${ctx.stash.amplifyApiEnvironmentName}`]; +} +``` + +## Step 3 - Invoke the custom query or mutation + +From your generated Data client, you can find all your custom queries and mutations under the `client.queries.` and `client.mutations.` APIs respectively. + +```ts +const { data, errors } = await client.mutations.BatchCreatePost({ + contents: ['Post 1', 'Post 2', 'Post 3'] +}); +``` diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-polly/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-polly/index.mdx index ccd7697d650..03f73f3c676 100644 --- a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-polly/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-polly/index.mdx @@ -218,6 +218,7 @@ Amplify.configure(outputs); Example frontend code to create an audio buffer for playback using a text input. + ```ts title="App.tsx" import "./App.css"; import { generateClient } from "aws-amplify/api"; @@ -267,4 +268,53 @@ function App() { export default App; ``` + + +```ts title="app.component.ts" +import type { Schema } from '../../../amplify/data/resource'; +import { Component } from '@angular/core'; +import { generateClient } from 'aws-amplify/api'; +import { getUrl } from 'aws-amplify/storage'; + +const client = generateClient(); + +type PollyReturnType = Schema['convertTextToSpeech']['returnType']; + +@Component({ + selector: 'app-root', + template: ` +
+ + + Get audio file +
+ `, + styleUrls: ['./app.component.css'], +}) +export class App { + src: string = ''; + file: PollyReturnType = ''; + + async synthesize() { + const { data, errors } = await client.mutations.convertTextToSpeech({ + text: 'Hello World!', + }); + + if (!errors && data) { + this.file = data; + } else { + console.log(errors); + } + } + + async fetchAudio() { + const res = await getUrl({ + path: 'public/' + this.file, + }); + + this.src = res.url.toString(); + } +} +``` +
diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-rekognition/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-rekognition/index.mdx index 61378979b58..91e4d3db8a5 100644 --- a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-rekognition/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-amazon-rekognition/index.mdx @@ -219,6 +219,7 @@ Amplify.configure(outputs); This code sets up a React app to upload an image to an S3 bucket and then use Amazon Rekognition to recognize the text in the uploaded image. + ```ts title="App.tsx" import { type ChangeEvent, useState } from "react"; import { generateClient } from "aws-amplify/api"; @@ -282,3 +283,71 @@ function App() { export default App; ``` + + +```ts title="app.component.ts" +import type { Schema } from '../../../amplify/data/resource'; +import { Component } from '@angular/core'; +import { generateClient } from 'aws-amplify/api'; +import { uploadData } from 'aws-amplify/storage'; +import { CommonModule } from '@angular/common'; + +// Generating the client +const client = generateClient(); + +type IdentifyTextReturnType = Schema['identifyText']['returnType']; + +@Component({ + selector: 'app-text-recognition', + standalone: true, + imports: [CommonModule], + template: ` +
+

Amazon Rekognition Text Recognition

+
+ + +
+

Recognized Text:

+ {{ textData }} +
+
+
+ `, +}) +export class TodosComponent { + // Component properties instead of React state + path: string = ''; + textData?: IdentifyTextReturnType; + + // Function to handle file upload to S3 bucket + async handleTranslate(event: Event) { + const target = event.target as HTMLInputElement; + if (target.files && target.files.length > 0) { + const file = target.files[0]; + const s3Path = 'public/' + file.name; + + try { + await uploadData({ + path: s3Path, + data: file, + }); + + this.path = s3Path; + } catch (error) { + console.error(error); + } + } + } + + // Function to recognize text from the uploaded image + async recognizeText() { + // Identifying text in the uploaded image + const { data } = await client.queries.identifyText({ + path: this.path, // File name + }); + this.textData = data; + } +} +``` +
diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-bedrock/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-bedrock/index.mdx index b141491b60b..3c5727deb69 100644 --- a/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-bedrock/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/connect-bedrock/index.mdx @@ -350,6 +350,7 @@ const { data, errors } = await client.queries.generateHaiku({ Here's an example of a simple UI that prompts a generative AI model to create a haiku based on user input: + ```tsx title="App.tsx" import type { Schema } from '@/amplify/data/resource'; import type { FormEvent } from 'react'; @@ -402,6 +403,65 @@ export default function App() { ); } ``` + + + +```ts title="app.component.ts" +import type { Schema } from '../../../amplify/data/resource'; +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Amplify } from 'aws-amplify'; +import { generateClient } from 'aws-amplify/api'; +import outputs from '../../../amplify_outputs.json'; + +Amplify.configure(outputs); + +const client = generateClient(); + +@Component({ + selector: 'app-haiku', + standalone: true, + imports: [FormsModule], + template: ` +
+
+

Haiku Generator

+
+ +
+
+
{{ answer }}
+
+
+
+ `, +}) +export class HaikuComponent { + prompt: string = ''; + answer: string | null = null; + + async sendPrompt() { + const { data, errors } = await client.queries.generateHaiku({ + prompt: this.prompt, + }); + + if (!errors) { + this.answer = data; + this.prompt = ''; + } else { + console.log(errors); + } + } +} +``` +
![A webpage titled "Haiku Generator" and input field. "Frank Herbert's Dune" is entered and submitted. Shortly after, a haiku is rendered to the page.](/images/haiku-generator.gif) diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/index.mdx index 585d0749a2f..af509d70506 100644 --- a/src/pages/[platform]/build-a-backend/data/custom-business-logic/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/index.mdx @@ -184,6 +184,8 @@ export const data = defineData({ }); ``` +If you want to use an existing lambda function, you can reference it by its name: `a.handler.function('name-of-existing-lambda-fn')`. This references an external lambda resource which Amplify is not aware of. You need to make sure the function and all dependencies are being managed outside of Amplify. +
diff --git a/src/pages/[platform]/build-a-backend/data/custom-business-logic/search-and-aggregate-queries/index.mdx b/src/pages/[platform]/build-a-backend/data/custom-business-logic/search-and-aggregate-queries/index.mdx index c9cd10d3deb..cd4d4a3d9fe 100644 --- a/src/pages/[platform]/build-a-backend/data/custom-business-logic/search-and-aggregate-queries/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/custom-business-logic/search-and-aggregate-queries/index.mdx @@ -905,7 +905,7 @@ You can also check this in the DynamoDB console by going to the Integrations sec ## Step 4: Expose new queries on OpenSearch -### Step 4a:Add OpenSearch Datasource to backend +### Step 4a: Add OpenSearch Datasource to backend First, Add the OpenSearch data source to the data backend. Add the following code to the end of the `amplify/backend.ts` file. diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/index.mdx index e0d025f2572..04da21c3ebf 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/index.mdx @@ -35,19 +35,16 @@ Function access to `defineData` can be configured using an authorization rule on import { a, defineData, - defineFunction, type ClientSchema } from '@aws-amplify/backend'; - -const functionWithDataAccess = defineFunction({ - entry: '../functions/data-access.ts' -}); +import { functionWithDataAccess } from '../function/data-access/resource'; const schema = a .schema({ Todo: a.model({ name: a.string(), - description: a.string() + description: a.string(), + isDone: a.boolean() }) }) // highlight-next-line @@ -60,14 +57,25 @@ export const data = defineData({ }); ``` +Create a new directory and a resource file, `amplify/functions/data-access/resource.ts`. Then, define the Function with `defineFunction`: + +```ts title="amplify/functions/data-access/resource.ts" +import { defineFunction } from '@aws-amplify/backend'; + +export const functionWithDataAccess = defineFunction({ + name: 'data-access', +}); +``` + The object returned from `defineFunction` can be passed directly to `allow.resource()` in the schema authorization rules. This will grant the function the ability to execute Query, Mutation, and Subscription operations against the GraphQL API. Use the `.to()` method to narrow down access to one or more operations. -```ts +```ts title="amplify/data/resource.ts" const schema = a .schema({ Todo: a.model({ name: a.string(), - description: a.string() + description: a.string(), + isDone: a.boolean() }) }) // highlight-start @@ -77,8 +85,6 @@ const schema = a // highlight-end ``` -When configuring function access, the function will be provided the API endpoint as an environment variable named `_GRAPHQL_ENDPOINT`, where `defineDataName` is transformed to SCREAMING_SNAKE_CASE. The default name is `AMPLIFY_DATA_GRAPHQL_ENDPOINT` unless you have specified a different name in `defineData`. - Function access can only be configured on the schema object. It cannot be configured on individual models or fields. @@ -89,64 +95,27 @@ Function access can only be configured on the schema object. It cannot be config In the handler file for your function, configure the Amplify data client -```ts title="amplify/functions/data-access.ts" +```ts title="amplify/functions/data-access/handler.ts" +import type { Handler } from 'aws-lambda'; +import type { Schema } from '../../data/resource'; import { Amplify } from 'aws-amplify'; import { generateClient } from 'aws-amplify/data'; -import { Schema } from '../data/resource'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; import { env } from '$amplify/env/'; // replace with your function name +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env); -Amplify.configure( - { - API: { - GraphQL: { - endpoint: env._GRAPHQL_ENDPOINT, // replace with your defineData name - region: env.AWS_REGION, - defaultAuthMode: 'identityPool' - } - } - }, - { - Auth: { - credentialsProvider: { - getCredentialsAndIdentityId: async () => ({ - credentials: { - accessKeyId: env.AWS_ACCESS_KEY_ID, - secretAccessKey: env.AWS_SECRET_ACCESS_KEY, - sessionToken: env.AWS_SESSION_TOKEN, - }, - }), - clearCredentialsAndIdentityId: () => { - /* noop */ - }, - }, - }, - } -); - -const dataClient = generateClient(); +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); export const handler = async (event) => { // your function code goes here } ``` -Use the command below to generate GraphQL client code to call your data backend. - - -**Note**: We are working on bringing the end-to-end typed experience to connect to your data from within function resources without needing this step. If you'd like to provide feedback the experience or have early access, join our [Discord community](https://discord.gg/amplify). - - - -```sh title="Terminal" showLineNumbers={false} -npx ampx generate graphql-client-code --out /graphql -``` - - - -**Note:** Whenever you update your data model, you will need to run the command above again. - +When configuring Amplify with `getAmplifyDataClientConfig`, your function consumes schema information from an S3 bucket created during backend deployment with grants for the access your function need to use it. Any changes to this bucket outside of backend deployment may break your function. Once you have generated the client code, update the function to access the data. The following code creates a todo and then lists all todos. @@ -154,21 +123,15 @@ Once you have generated the client code, update the function to access the data. ```ts title="amplify/functions/data-access.ts" const client = generateClient(); -export const handler = async (event) => { - await client.graphql({ - query: createTodo, - variables: { - input: { - name: "My first todo", - description: "This is my first todo", - }, - }, - }); - - - await client.graphql({ - query: listTodos, - }); +export const handler: Handler = async (event) => { + const { errors: createErrors, data: newTodo } = await client.models.Todo.create({ + name: "My new todo", + description: "Todo description", + isDone: false, + }) + + + const { errors: listErrors, data: todos } = await client.models.Todo.list(); return event; }; diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/public-data-access/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/public-data-access/index.mdx index df2beb9f9f6..35cdcc15d1c 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/public-data-access/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/public-data-access/index.mdx @@ -79,7 +79,7 @@ In your application, you can perform CRUD operations against the model by specif try { final todo = Todo(content: 'My new todo'); final request = ModelMutations.create( - todo, + todo, authorizationMode: APIAuthorizationType.apiKey, ); final createdTodo = await Amplify.API.mutations(request: request).response; @@ -112,6 +112,52 @@ do { +### Extend API Key Expiration + +If the API key has not expired, you can extend the expiration date by deploying your app again. The API key expiration date will be set to `expiresInDays` days from the date when the app is deployed. In the example below, the API key will expire 7 days from the latest deployment. + +```ts title="amplify/data/resource.ts" +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + apiKeyAuthorizationMode: { + expiresInDays: 7, + }, + }, +}); +``` + +### Rotate an API Key + +You can rotate an API key if it was expired, compromised, or deleted. To rotate an API key, you can override the logical ID of the API key resource in the `amplify/backend.ts` file. This will create a new API key with a new logical ID. + +```ts title="amplify/backend.ts" +const backend = defineBackend({ + auth, + data, +}); + +backend.data.resources.cfnResources.cfnApiKey?.overrideLogicalId( + `recoverApiKey${new Date().getTime()}` +); +``` + +Deploy your app. After the deploy has finished, remove the override to the logical ID and deploy your app again to use the default logical ID. + +```ts title="amplify/backend.ts" +const backend = defineBackend({ + auth, + data, +}); + +// backend.data.resources.cfnResources.cfnApiKey?.overrideLogicalId( +// `recoverApiKey${new Date().getTime()}` +// ); +``` + +A new API key will be created for your app. + ## Add public authorization rule using Amazon Cognito identity pool's unauthenticated role You can also override the authorization provider. In the example below, `identityPool` is specified as the provider which allows you to use an "Unauthenticated Role" from the Cognito identity pool for public access instead of an API key. Your Auth resources defined in `amplify/auth/resource.ts` generates scoped down IAM policies for the "Unauthenticated role" in the Cognito identity pool automatically. @@ -182,7 +228,7 @@ In your application, you can perform CRUD operations against the model with the try { final todo = Todo(content: 'My new todo'); final request = ModelMutations.create( - todo, + todo, authorizationMode: APIAuthorizationType.iam, ); final createdTodo = await Amplify.API.mutations(request: request).response; diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access/index.mdx index 934b3421718..3c32f50abbc 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access/index.mdx @@ -168,3 +168,19 @@ By default, `group` authorization leverages Amazon Cognito user pool groups but - subscriptions are only supported if the user is part of 20 or fewer groups - you can only authorize 20 or fewer user groups per record + +## Access user groups from the session + + + +You can access a user's groups from their session using the Auth category: + +```ts +import { fetchAuthSession } from 'aws-amplify/auth'; + +const session = await fetchAuthSession(); +const groups = session.tokens.accessToken.payload['cognito:groups'] || []; + +console.log('User groups:', groups); +``` + diff --git a/src/pages/[platform]/build-a-backend/data/data-modeling/relationships/index.mdx b/src/pages/[platform]/build-a-backend/data/data-modeling/relationships/index.mdx index 7e3934edfcf..f36b8f92d7d 100644 --- a/src/pages/[platform]/build-a-backend/data/data-modeling/relationships/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/data-modeling/relationships/index.mdx @@ -439,6 +439,29 @@ do { + + +### Handling orphaned foreign keys on parent record deletion in "Has Many" relationship + +```ts +// Get the IDs of the related members. +const { data: teamWithMembers } = await client.models.Team.get( + { id: teamId }, + { selectionSet: ["id", "members.*"] }, +); + +// Delete Team +await client.models.Team.delete({ id: teamWithMembers.id }); + +// Delete all members in parallel +await Promise.all( + teamWithMembers.members.map(member => + client.models.Member.delete({ id: member.id }) +)); +``` + + + ## Model a "one-to-one" relationship Create a one-to-one relationship between two models using the `hasOne()` and `belongsTo()` methods. In the example below, a **Customer** has a **Cart** and a *Cart* belongs to a **Customer**. @@ -794,6 +817,26 @@ val cart = Amplify.API.query( + + +### Handling orphaned foreign keys on parent record deletion in "Has One" relationship + +```ts +// Get the customer with their associated cart +const { data: customerWithCart } = await client.models.Customer.get( + { id: customerId }, + { selectionSet: ["id", "activeCart.*"] }, +); + +// Delete Cart if exists +await client.models.Cart.delete({ id: customerWithCart.activeCart.id }); + +// Delete the customer +await client.models.Customer.delete({ id: customerWithCart.id }); +``` + + + ## Model a "many-to-many" relationship In order to create a many-to-many relationship between two models, you have to create a model that serves as a "join table". This "join table" should contain two one-to-many relationships between the two related entities. For example, to model a **Post** that has many **Tags** and a **Tag** has many **Posts**, you'll need to create a new **PostTag** model that represents the relationship between these two entities. diff --git a/src/pages/[platform]/build-a-backend/data/enable-logging/index.mdx b/src/pages/[platform]/build-a-backend/data/enable-logging/index.mdx new file mode 100644 index 00000000000..71080dc4856 --- /dev/null +++ b/src/pages/[platform]/build-a-backend/data/enable-logging/index.mdx @@ -0,0 +1,111 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Enable logging', + description: 'Learn how to enable logging for your Amplify data resource', + platforms: [ + 'android', + 'angular', + 'flutter', + 'javascript', + 'nextjs', + 'react', + 'react-native', + 'swift', + 'vue' + ] +}; + +export function getStaticPaths() { + return getCustomStaticPath(meta.platforms); +} + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +You can enable logging to debug your GraphQL API using Amazon CloudWatch logs. To learn more about logging and monitoring capabilities for your GraphQL API, visit the [AWS AppSync documentation for logging and monitoring](https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html). + +## Enable default logging configuration + +Default logging can be enabled by setting the `logging` property to `true` in the call to `defineData`. For example: + +```ts title="amplify/data/resource.ts" +export const data = defineData({ + // ... + logging: true +}); +``` + +Using `logging: true` applies the default configuration: +- `excludeVerboseContent: true` (see [AppSync's Request-level logs](https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#cwl)) +- `fieldLogLevel: 'none'` (see [AppSync's Field-level logs](https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#cwl)) +- `retention: '1 week'` (see [Enum RetentionDays](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.RetentionDays.html)) + +## Customize logging configuration + +You can customize individual configuration values by providing a [`DataLogConfig`](#datalogconfig-fields) object. For example: + +```ts title="amplify/data/resource.ts" +export const data = defineData({ + // ... + logging: { + excludeVerboseContent: false, + fieldLogLevel: 'all', + retention: '1 month' + } +}); +``` + + +**WARNING**: Setting `excludeVerboseContent` to `false` logs full queries and user parameters, which can contain sensitive data. We recommend limiting CloudWatch log access to only those roles or users (e.g., DevOps or developers) who genuinely require it, by carefully scoping your IAM policies. + + +## Configuration Properties + +### `logging` +- `true`: Enables default logging. +- `DataLogConfig` object: Overrides one or more default fields. + +### `DataLogConfig` Fields + +- **`excludeVerboseContent?: boolean`** + - Defaults to `true` + - When `false`, logs can contain request-level logs. See [AppSync's Request-Level Logs](https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#cwl). + +- **`fieldLogLevel?: DataLogLevel`** + - Defaults to `'none'` + - Supported values of [AppSync's Field Log Levels](https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#cwl): + - `'none'` + - `'error'` + - `'info'` + - `'debug'` + - `'all'` + +- **`retention?: LogRetention`** + - Number of days to keep the logs + - Defaults to `'1 week'` + - Supported values of [Enum RetentionDays](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.RetentionDays.html): + - `'1 day'` + - `'3 days'` + - `'5 days'` + - `'1 week'` + - `'2 weeks'` + - `'1 month'` + - `'2 months'` + - `'3 months'` + - `'4 months'` + - `'5 months'` + - `'6 months'` + - `'1 year'` + - `'13 months'` + - `'18 months'` + - `'2 years'` + - `'5 years'` + - `'10 years'` + - `'infinite'` diff --git a/src/pages/[platform]/build-a-backend/data/optimistic-ui/index.mdx b/src/pages/[platform]/build-a-backend/data/optimistic-ui/index.mdx index 7946cea3726..4b2bfa163ec 100644 --- a/src/pages/[platform]/build-a-backend/data/optimistic-ui/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/optimistic-ui/index.mdx @@ -43,8 +43,8 @@ For more on Amplify Data, see the [API documentation](/[platform]/build-a-backen To get started, run the following command in an existing Amplify project with a React frontend: ```bash title="Terminal" showLineNumbers={false} -# Install TanStack Query -npm i @tanstack/react-query @tanstack/react-query-devtools +npm add @tanstack/react-query && \ +npm add --save-dev @tanstack/react-query-devtools ``` Modify your Data schema to use this "Real Estate Property" example: diff --git a/src/pages/[platform]/build-a-backend/data/query-data/index.mdx b/src/pages/[platform]/build-a-backend/data/query-data/index.mdx index 0b1402143aa..19ccffbcfef 100644 --- a/src/pages/[platform]/build-a-backend/data/query-data/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/query-data/index.mdx @@ -147,6 +147,8 @@ const { }); ``` + + If you're building a React application, you can use the `usePagination` hook in Amplify UI to help with managing the pagination user experience. ```js @@ -187,6 +189,8 @@ export const PaginationHasMorePagesExample = () => { }; ``` + + **Limitations:** @@ -214,7 +218,11 @@ const { data: blogWithSubsetOfData, errors } = await client.models.Blog.get( ## TypeScript type helpers for Amplify Data -When using TypeScript, you frequently need to specify data model types for type generics. For instance, with React's `useState`, you provide a type in TypeScript to ensure type-safety in your component code using the state. Use the `Schema["MODEL_NAME"]["type"]` pattern to get TypeScript types for the shapes of data models returned from the backend API. This allows you to get consumable TypeScript types for the shapes of the data model return values coming from the backend API. +When using TypeScript, you frequently need to specify data model types for type generics. + + + +For instance, with React's `useState`, you provide a type in TypeScript to ensure type-safety in your component code using the state. Use the `Schema["MODEL_NAME"]["type"]` pattern to get TypeScript types for the shapes of data models returned from the backend API. ```ts import { type Schema } from '@/amplify/data/resource'; @@ -224,8 +232,21 @@ type Post = Schema['Post']['type']; const [posts, setPosts] = useState([]); ``` + + + + +```ts +import { type Schema } from '../../../amplify/data/resource'; + +type Post = Schema['Post']['type']; +``` + + + You can combine the `Schema["MODEL_NAME"]["type"]` type with the `SelectionSet` helper type to describe the return type of API requests using the `selectionSet` parameter: + ```ts import type { SelectionSet } from 'aws-amplify/data'; import type { Schema } from '../amplify/data/resource'; @@ -245,6 +266,86 @@ const fetchPosts = async () => { } ``` + + + +```ts + + + +``` + + + +```ts +import type { Schema } from '../../../amplify/data/resource'; +import { Component, OnInit } from '@angular/core'; +import { generateClient, type SelectionSet } from 'aws-amplify/data'; +import { CommonModule } from '@angular/common'; + +const client = generateClient(); + +const selectionSet = ['content', 'blog.author.*', 'comments.*'] as const; + +type PostWithComments = SelectionSet< + Schema['Post']['type'], + typeof selectionSet +>; + +@Component({ + selector: 'app-todos', + standalone: true, + imports: [CommonModule], + templateUrl: './todos.component.html', + styleUrls: ['./todos.component.css'], +}) +export class TodosComponent implements OnInit { + posts: PostWithComments[] = []; + + constructor() {} + + ngOnInit(): void { + this.fetchPosts(); + } + + async fetchPosts(): Promise { + const { data: postsWithComments } = await client.models.Post.list({ + selectionSet, + }); + this.posts = postsWithComments; + } +} +``` + + ## Cancel read requests You can cancel any query API request by calling `.cancel` on the query request promise that's returned by `.list(...)` or `.get(...)`. diff --git a/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx b/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx index 26542619284..01125c78b9e 100644 --- a/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/set-up-data/index.mdx @@ -383,8 +383,7 @@ Let's first add a button to create a new todo item. To make a "create Todo" API - - + ```tsx title="src/TodoList.tsx" import type { Schema } from '../amplify/data/resource' @@ -405,7 +404,6 @@ export default function TodoList() { } ``` - @@ -432,11 +430,41 @@ async function createTodo() { ``` - +Run the application in local development mode with `npm run dev` and check your network tab after creating a todo. You should see a successful request to a `/graphql` endpoint. + + + +Try playing around with the code completion of `.update(...)` and `.delete(...)` to get a sense of other mutation operations. + + + + + +```ts title="todo-list.component.ts" +import type { Schema } from '../amplify/data/resource'; +import { Component } from '@angular/core'; +import { generateClient } from 'aws-amplify/data'; +const client = generateClient(); + +@Component({ + selector: 'app-todo-list', + template: ` + + ` +}) +export class TodoListComponent { + async createTodo() { + await client.models.Todo.create({ + content: window.prompt("Todo content?"), + isDone: false + }); + } +} +``` Run the application in local development mode and check your network tab after creating a todo. You should see a successful request to a `/graphql` endpoint. @@ -445,8 +473,8 @@ Run the application in local development mode and check your network tab after c Try playing around with the code completion of `.update(...)` and `.delete(...)` to get a sense of other mutation operations. - + In your MainActivity, add a button to create a new todo. @@ -637,9 +665,7 @@ Creating Todo successful. Next, list all your todos and then refetch the todos after a todo has been added: - - - + ```tsx title="src/TodoList.tsx" import { useState, useEffect } from "react"; import type { Schema } from "../amplify/data/resource"; @@ -684,7 +710,6 @@ export default function TodoList() { - ```html title="src/TodoList.vue"