diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4bc3e60d1b8..e1ea17fb3d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -104,6 +104,7 @@ /src/themes @aws-amplify/documentation-team /src/utils @aws-amplify/documentation-team /tasks @aws-amplify/documentation-team +.github @aws-amplify/documentation-team #Protected Content /src/protected @reesscot @srquinn21 @Milan-Shah @swaminator diff --git a/.github/workflows/check_for_console_errors.yml b/.github/workflows/check_for_console_errors.yml new file mode 100644 index 00000000000..e3852ba97a0 --- /dev/null +++ b/.github/workflows/check_for_console_errors.yml @@ -0,0 +1,38 @@ +name: CheckConsoleErrors +on: + pull_request: + branches: [main] + types: [opened, synchronize] +permissions: + contents: read +jobs: + CheckConsoleErrors: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 https://github.com/actions/checkout/commit/b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Setup Node.js 20.x + uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 https://github.com/actions/setup-node/commit/e33196f7422957bea03ed53f6fbb155025ffc7b8 + with: + node-version: 20.x + - name: Install Dependencies + run: yarn + - name: Run Build + run: yarn build:release + env: + NODE_OPTIONS: --max_old_space_size=4096 + - name: Run Server + run: | + python -m http.server 3000 -d ${{ vars.BUILD_DIR }} & + sleep 5 + - name: Run Console Errors + id: consoleErrors + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 https://github.com/actions/github-script/commit/d7906e4ad0b1822421a7e6a35d5ca353c962f410 + with: + result-encoding: string + script: | + const { consoleErrors } = require('./tasks/console-errors.js'); + return await consoleErrors(); + - name: Fail if console errors have been found + if: ${{ steps.consoleErrors.outputs.result }} + run: exit 1 diff --git a/cspell.json b/cspell.json index c98d74d76c7..69817bdd7eb 100644 --- a/cspell.json +++ b/cspell.json @@ -485,6 +485,7 @@ "customizable", "customtabs", "customui", + "conver", "dabit", "dataaccess", "dataacess", @@ -916,6 +917,7 @@ "Neue", "NEW_PASSWORD_REQUIRED", "newHire", + "newimage", "NextActivity.class", "NextActivity", "nextActivityClass", @@ -1343,6 +1345,7 @@ "Untag", "updateapi", "updateauth", + "updatedimage", "UpdateTable", "updateTodo", "uploadData", @@ -1517,7 +1520,7 @@ "privatesaccess", "menudetaileditors", "editorgroupaccess", - "publicauthreadonly", + "authreadonly", "envs", "Onetoone", "onetomany", diff --git a/package.json b/package.json index 73ee7d22885..7cb97565ea8 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "@adobe/css-tools": "4.3.2", "follow-redirects": "^1.15.6", "ip": "2.0.1", - "sharp": "0.32.6" + "sharp": "0.32.6", + "ejs": "3.1.10" }, "scripts": { "clean": "rm -rf node_modules yarn.lock", diff --git a/public/images/console/7_publicauthreadonly.png b/public/images/console/7_authreadonly.png similarity index 100% rename from public/images/console/7_publicauthreadonly.png rename to public/images/console/7_authreadonly.png diff --git a/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx b/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx index 7c9989f7d91..eae43ec719c 100644 --- a/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx +++ b/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx @@ -21,12 +21,12 @@ describe('BlockSwitcher', () => { const blockSwitcher = await screen.findByText(blockAContent); expect(blockSwitcher).toBeInTheDocument(); }); - + it('should have more than one Block', async () => { render(component); expect(component.props.children.length).toBeGreaterThan(1); }); - + it('should show the first Block as default', async () => { render(component); const tabs = await screen.getAllByRole('tab'); @@ -39,7 +39,7 @@ describe('BlockSwitcher', () => { expect(panels[1]).not.toHaveClass('amplify-tabs__panel--active'); expect(panels[2]).not.toHaveClass('amplify-tabs__panel--active'); }); - + it('should load all Blocks to the DOM', async () => { render(component); const blockA = await screen.findByText(blockAContent); @@ -49,7 +49,7 @@ describe('BlockSwitcher', () => { expect(blockB).toBeInTheDocument(); expect(blockC).toBeInTheDocument(); }); - + it('should switch tabs upon click', async () => { render(component); const tabs = await screen.getAllByRole('tab'); diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index 4260480485d..ab55076eb7e 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -39,7 +39,7 @@ export const directory = { path: 'src/pages/[platform]/start/manual-installation/index.mdx' }, { - path: 'src/pages/[platform]/start/gen1-gen2/index.mdx' + path: 'src/pages/[platform]/start/migrate-to-gen2/index.mdx' } ] }, @@ -144,6 +144,9 @@ export const directory = { { path: 'src/pages/[platform]/build-a-backend/auth/multi-step-sign-in/index.mdx' }, + { + path: 'src/pages/[platform]/build-a-backend/auth/sign-in-with-web-ui/index.mdx' + }, { path: 'src/pages/[platform]/build-a-backend/auth/app-uninstall/index.mdx' }, @@ -158,6 +161,9 @@ export const directory = { }, { path: 'src/pages/[platform]/build-a-backend/auth/moving-to-production/index.mdx' + }, + { + path: 'src/pages/[platform]/build-a-backend/auth/advanced-workflows/index.mdx' } ] }, @@ -258,6 +264,9 @@ export const directory = { } ] }, + { + path: 'src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx' + }, { path: 'src/pages/[platform]/build-a-backend/data/custom-subscription/index.mdx' }, 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 7605dc1c46d..f4ff0a66bae 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 @@ -43,7 +43,7 @@ An application with Amplify libraries integrated and a minimum target of any of -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. +visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). @@ -75,7 +75,7 @@ For more information on how to use the `visionos-preview` branch, see [Platform ## Set up Analytics backend -Use the [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html) to create an analytics resource powered by [Amazon Pinpoint](https://aws.amazon.com/pinpoint/). +Use the [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html) to create an analytics resource powered by [Amazon Pinpoint](https://aws.amazon.com/pinpoint/). ```ts title="amplify/backend.ts" import { auth } from "./auth/resource"; @@ -85,9 +85,9 @@ import { CfnApp } from "aws-cdk-lib/aws-pinpoint"; import { Stack } from "aws-cdk-lib/core"; const backend = defineBackend({ - auth, + auth, data, - // additional resources + // additional resources }); const analyticsStack = backend.createStack("analytics-stack"); @@ -195,7 +195,7 @@ Amplify.configure({ ``` - + ```js title="pages/_app.tsx" import { Amplify } from 'aws-amplify'; import { record } from 'aws-amplify/analytics'; @@ -230,7 +230,7 @@ init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSPinpointAnalyticsPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with Auth and Analytics plugins") } catch { print("Failed to initialize Amplify with \(error)") @@ -260,7 +260,7 @@ func application( do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSPinpointAnalyticsPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with Auth and Analytics plugins") } catch { print("Failed to initialize Amplify with \(error)") @@ -367,7 +367,7 @@ Future main() async { class MyApp extends StatefulWidget { const MyApp({Key? key}): super(key: key); - + // ... } ``` @@ -378,6 +378,16 @@ To initialize the Amplify Auth and Analytics categories you call `Amplify.addPlu Add the following code to your `onCreate()` method in your application class: + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + @@ -387,6 +397,8 @@ import com.amplifyframework.AmplifyException; import com.amplifyframework.analytics.pinpoint.AWSPinpointAnalyticsPlugin; import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; import com.amplifyframework.core.Amplify; +import com.amplifyframework.core.configuration.AmplifyOutputs; + ``` ```java @@ -406,7 +418,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSPinpointAnalyticsPlugin plugins Amplify.addPlugin(new AWSCognitoAuthPlugin()); Amplify.addPlugin(new AWSPinpointAnalyticsPlugin()); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -425,7 +437,8 @@ import com.amplifyframework.AmplifyException import com.amplifyframework.analytics.pinpoint.AWSPinpointAnalyticsPlugin import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.core.Amplify -``` +import com.amplifyframework.core.configuration.AmplifyOutputs +``` ```kotlin Amplify.addPlugin(AWSCognitoAuthPlugin()) @@ -443,7 +456,7 @@ class MyAmplifyApp : Application() { // Add these lines to add the AWSCognitoAuthPlugin and AWSPinpointAnalyticsPlugin plugins Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSPinpointAnalyticsPlugin()) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { @@ -461,6 +474,7 @@ import android.util.Log; import com.amplifyframework.AmplifyException; import com.amplifyframework.analytics.pinpoint.AWSPinpointAnalyticsPlugin; import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.rx.RxAmplify; ``` @@ -481,7 +495,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSPinpointAnalyticsPlugin plugins RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); RxAmplify.addPlugin(new AWSPinpointAnalyticsPlugin()); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/geo/configure-geofencing/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/geo/configure-geofencing/index.mdx index 14a90b78983..fd7c4220bf3 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/geo/configure-geofencing/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/geo/configure-geofencing/index.mdx @@ -4,12 +4,10 @@ export const meta = { title: 'Configure a geofence collection', description: 'Create and manage collections of Geofences', platforms: [ - 'android', 'angular', 'javascript', 'nextjs', 'react', - 'swift', 'vue' ] }; @@ -100,7 +98,7 @@ backend.addOutput({ ## Geofence Collection Pricing Plan -The pricing plan for the Geofence Collection will be set to `RequestBasedUsage`. We advice you to go through the [location service pricing](https://aws.amazon.com/location/pricing/) along with the [location service terms](https://aws.amazon.com/service-terms/) (_82.5 section_) to learn more about the pricing plan. +The pricing plan for the Geofence Collection will be set to `RequestBasedUsage`. We advice you to go through the [location service pricing](https://aws.amazon.com/location/pricing/) along with the [location service terms](https://aws.amazon.com/service-terms/) (_82.5 section_) to learn more about the pricing plan. #### Group access diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/geo/set-up-geo/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/geo/set-up-geo/index.mdx index 46dc1b24f9c..e54382a317c 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/geo/set-up-geo/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/geo/set-up-geo/index.mdx @@ -101,12 +101,12 @@ backend.addOutput({ ## Configure your application - + To display a map in your application, you can use the [Amplify React MapView component](https://ui.docs.amplify.aws/react/components/geo) or the [MapLibre GL](https://github.com/maplibre/maplibre-gl-js) with `maplibre-gl-js-amplify` libraries are required. Install the necessary dependencies by running the following command: -```bash title="Terminal" showLineNumbers={false} +```bash title="Terminal" showLineNumbers={false} npm add aws-amplify @aws-amplify/geo ``` @@ -164,7 +164,7 @@ For a full example, please follow the [project setup walkthrough](/[platform]/st -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. +visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). @@ -219,13 +219,23 @@ To initialize Amplify Geo, use the `Amplify.addPlugin()` method to add the AWS L Add the following code to your `onCreate()` method in your application class: + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + ```java Amplify.addPlugin(new AWSCognitoAuthPlugin()); Amplify.addPlugin(new AWSLocationGeoPlugin()); -Amplify.configure(getApplicationContext()); +Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); ``` Your class will look like this: @@ -239,7 +249,7 @@ public class MyAmplifyApp extends Application { try { Amplify.addPlugin(new AWSCognitoAuthPlugin()); Amplify.addPlugin(new AWSLocationGeoPlugin()); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error); @@ -254,7 +264,7 @@ public class MyAmplifyApp extends Application { ```kotlin Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSLocationGeoPlugin()) -Amplify.configure(applicationContext) +Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), applicationContext) ``` Your class will look like this: @@ -267,7 +277,7 @@ class MyAmplifyApp : Application() { try { Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSLocationGeoPlugin()) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error) @@ -282,7 +292,7 @@ class MyAmplifyApp : Application() { ```java RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); RxAmplify.addPlugin(new AWSLocationGeoPlugin()); -RxAmplify.configure(getApplicationContext()); +RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); ``` Your class will look like this: @@ -296,7 +306,7 @@ public class MyAmplifyApp extends Application { try { RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); RxAmplify.addPlugin(new AWSLocationGeoPlugin()); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error); @@ -331,7 +341,7 @@ func configureAmplify() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSLocationGeoPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Initialized Amplify"); } catch { print("Could not initialize Amplify: \(error)") @@ -382,7 +392,7 @@ Initialized Amplify -**Notes:** +**Notes:** - If you want to use existing Amazon Location Service resources [follow this guide](/[platform]/build-a-backend/add-aws-services/geo/existing-resources/) instead. - If you want to use Amazon Location Service APIs not directly supported by Geo, use the [escape hatch](/[platform]/build-a-backend/add-aws-services/geo/amazon-location-sdk/) to access the Amazon Location Service SDK. diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/logging/change-local-storage/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/logging/change-local-storage/index.mdx index 24cf12be44e..53881e4fc94 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/logging/change-local-storage/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/logging/change-local-storage/index.mdx @@ -90,7 +90,7 @@ do { let loggingConfiguration = AWSCloudWatchLoggingPluginConfiguration(logGroupName: "", region: "", localStoreMaxSizeInMB: 2) let loggingPlugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: loggingConfiguration) try Amplify.add(plugin: loggingPlugin) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { assert(false, "Error initializing Amplify: \(error)") } diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/logging/flush-logs/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/logging/flush-logs/index.mdx index 514d5e80e69..41c867d902a 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/logging/flush-logs/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/logging/flush-logs/index.mdx @@ -122,7 +122,7 @@ do { let loggingConfiguration = AWSCloudWatchLoggingPluginConfiguration(logGroupName: "", region: "", flushIntervalInSeconds: 120) let loggingPlugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: loggingConfiguration) try Amplify.add(plugin: loggingPlugin) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { assert(false, "Error initializing Amplify: \(error)") } @@ -145,7 +145,7 @@ You can choose at anytime to flush the log messages that are saved locally on th - + @@ -227,7 +227,7 @@ import AWSCloudWatchLoggingPlugin Execute the flush log function from the plugin. ```swift -let cloudWatchPlugin = try Amplify.Logging.getPlugin(for: "awsCloudWatchLoggingPlugin") as? AWSCloudWatchLoggingPlugin +let cloudWatchPlugin = try Amplify.Logging.getPlugin(for: "awsCloudWatchLoggingPlugin") as? AWSCloudWatchLoggingPlugin try await cloudWatchPlugin?.flushLogs() ``` diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/logging/remote-configuration/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/logging/remote-configuration/index.mdx index 30b7859328d..60fe13c743f 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/logging/remote-configuration/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/logging/remote-configuration/index.mdx @@ -316,7 +316,7 @@ do { let remoteConfigProvider = DefaultRemoteLoggingConstraintsProvider(endpoint: endpointUrl, region: "") let loggingPlugin = AWSCloudWatchLoggingPlugin(remoteLoggingConstraintsProvider: remoteConfigProvider) try Amplify.add(plugin: loggingPlugin) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { assert(false, "Error initializing Amplify: \(error)") } diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/logging/set-up-logging/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/logging/set-up-logging/index.mdx index 5e90fe6d9e1..11cb79a200f 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/logging/set-up-logging/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/logging/set-up-logging/index.mdx @@ -53,7 +53,7 @@ For a full example, please follow the [mobile support walkthrough](/swift/start/ -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. +visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). @@ -156,12 +156,24 @@ The `` and `` is the value you specified in the CDK cons To use the Amplify Logger and Amplify Auth categories in your app, you need to create and configure their corresponding plugins by calling the `Amplify.addPlugin()` and `Amplify.configure()` methods. Add the following imports to the top of your main `Application` file: + + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + ```java import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; import com.amplifyframework.core.Amplify; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin; ``` @@ -182,7 +194,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSCloudWatchLoggingPlugin plugins Amplify.addPlugin(new AWSCognitoAuthPlugin()); Amplify.addPlugin(new AWSCloudWatchLoggingPlugin()); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -196,8 +208,9 @@ public class MyAmplifyApp extends Application { ```kotlin -import com.amplifyframework.core.Amplify import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.configuration.AmplifyOutputs import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin ``` @@ -219,7 +232,7 @@ class MyAmplifyApp : Application() { // Add these lines to add the AWSCognitoAuthPlugin and AWSCloudWatchLoggingPlugin plugins Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSCloudWatchLoggingPlugin()) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { @@ -234,6 +247,7 @@ class MyAmplifyApp : Application() { ```java import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.rx.RxAmplify; import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin; ``` @@ -255,7 +269,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSCloudWatchLoggingPlugin plugins RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); RxAmplify.addPlugin(new AWSCloudWatchLoggingPlugin()); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -271,7 +285,7 @@ public class MyAmplifyApp extends Application { {' '} - + To use the Amplify Logger and Amplify Auth categories in your app, you need to create and configure their corresponding plugins by calling the `Amplify.addPlugin()` and `Amplify.configure()` methods. Add the following imports to the top of your main `Application` file: @@ -281,6 +295,7 @@ Add the following imports to the top of your main `Application` file: ```java import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; import com.amplifyframework.core.Amplify; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin; ``` Add the following code to its initializer. If there is none, you can create a default. The `` and `` are values you specified in the CDK construct as part of provisioning your backend resources. These values can also be found at the end of the output logs when deploying the sample CDK construct. @@ -304,7 +319,7 @@ public class MyAmplifyApp extends Application { Amplify.addPlugin(new AWSCognitoAuthPlugin()); AWSCloudWatchLoggingPluginConfiguration config = new AWSCloudWatchLoggingPluginConfiguration (,,1,60); Amplify.addPlugin(new AWSCloudWatchLoggingPlugin(config)); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -319,6 +334,7 @@ public class MyAmplifyApp extends Application { ```kotlin import com.amplifyframework.core.Amplify +import com.amplifyframework.core.configuration.AmplifyOutputs import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin ``` @@ -343,7 +359,7 @@ class MyAmplifyApp : Application() { Amplify.addPlugin(AWSCognitoAuthPlugin()) val config = AWSCloudWatchLoggingPluginConfiguration(logGroupName = , region = , localStoreMaxSizeInMB = 1, flushIntervalInSeconds = 60) Amplify.addPlugin(AWSCloudWatchLoggingPlugin(config)) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { @@ -358,6 +374,7 @@ class MyAmplifyApp : Application() { ```java import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.rx.RxAmplify; import com.amplifyframework.logging.cloudwatch.AWSCloudWatchLoggingPlugin; ``` @@ -381,7 +398,7 @@ public class MyAmplifyApp extends Application { RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); AWSCloudWatchLoggingPluginConfiguration config = new AWSCloudWatchLoggingPluginConfiguration (,,1,60); RxAmplify.addPlugin(new AWSCloudWatchLoggingPlugin(config)); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -440,7 +457,7 @@ init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSCloudWatchLoggingPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { assert(false, "Error initializing Amplify: \(error)") } @@ -462,7 +479,7 @@ import AWSCognitoAuthPlugin import AWSCloudWatchLoggingPlugin ``` -Add the following code to its initializer. If there is none, you can create a default `init`. The `` and `` are values you specified in the CDK construct as part of provisioning your backend resources. These values can also be found at the end of the output logs when deploying the sample CDK construct. +Add the following code to its initializer. If there is none, you can create a default `init`. The `` and `` are values you specified in the CDK construct as part of provisioning your backend resources. These values can also be found at the end of the output logs when deploying the sample CDK construct. The example below configures the logging plugin to automatically send all logs at log level `ERROR` at 60 seconds interval and store logs locally up to 1MB. ```swift @@ -471,7 +488,7 @@ init() { let loggingConfiguration = AWSCloudWatchLoggingPluginConfiguration(logGroupName: "", region: "", localStoreMaxSizeInMB: 1, flushIntervalInSeconds: 60) let loggingPlugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: loggingConfiguration) try Amplify.add(plugin: loggingPlugin) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { assert(false, "Error initializing Amplify: \(error)") } diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/overriding-resources/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/overriding-resources/index.mdx index 9c6812a3e18..e7cc8162461 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/overriding-resources/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/overriding-resources/index.mdx @@ -74,12 +74,6 @@ Most L1 and L2 AWS CDK constructs that are used by the `define*` functions are a ## Example - Grant access permissions between resources - - -**Under active development**: We are working to improve the experience of granting one resource access to another. For the time being, overrides can be used to achieve this. - - - Consider the case that we want to grant a function created by `defineFunction` access to call the Cognito user pool created by `defineAuth`. This can be accomplished with the following overrides. ```ts title="amplify/backend.ts" diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-entity/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-entity/index.mdx index 5dcefda788f..b7e4198ff01 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-entity/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-entity/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Identify entities from images', description: 'Learn how to identify entities from an image using Amplify.', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -35,268 +34,6 @@ export function getStaticProps(context) { ## Working with the API - - -### Detect entities in an image - -To detect general entities like facial features, landmarks etc, default configuration from CLI workflow will suffice (i.e. celebrity detection enabled & entity identification from collection disabled). - -Amplify will now detect general entity features when `IdentifyActionType.DETECT_ENTITIES` is passed in. Results are mapped to `IdentifyEntitiesResult`. For example: - - - - -```java -public void detectEntities(Bitmap image) { - Amplify.Predictions.identify( - IdentifyActionType.DETECT_ENTITIES, - image, - result -> { - IdentifyEntitiesResult identifyResult = (IdentifyEntitiesResult) result; - EntityDetails metadata = identifyResult.getEntities().get(0); - Log.i("MyAmplifyApp", metadata.getBox().toShortString()); - }, - error -> Log.e("MyAmplifyApp", "Entity detection failed", error) - ); -} -``` - - - - -```kotlin -fun detectEntities(image: Bitmap) { - Amplify.Predictions.identify(IdentifyActionType.DETECT_ENTITIES, image, - { result -> - val identifyResult = result as IdentifyEntitiesResult - val metadata = identifyResult.entities.firstOrNull() - Log.i("MyAmplifyApp", "${metadata?.box?.toShortString()}") - }, - { Log.e("MyAmplifyApp", "Entity detection failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectEntities(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(IdentifyActionType.DETECT_ENTITIES, image) - val identifyResult = result as IdentifyEntitiesResult - val value = identifyResult.entities.firstOrNull()?.box?.toShortString() - Log.i("MyAmplifyApp", "$value") - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Entity detection failed", error) - } -} -``` - - - - -```java -public void detectEntities(Bitmap image) { - RxAmplify.Predictions.identify(IdentifyActionType.DETECT_ENTITIES, image) - .subscribe( - result -> { - IdentifyEntitiesResult identifyResult = (IdentifyEntitiesResult) result; - EntityDetails metadata = identifyResult.getEntities().get(0); - Log.i("MyAmplifyApp", metadata.getBox().toShortString()); - }, - error -> Log.e("MyAmplifyApp", "Entity detection failed", error) - ); -} -``` - - - - -As a result of passing in an image, the bounding box ([`android.graphics.RectF`](https://developer.android.com/reference/android/graphics/RectF)) that captures detected entity will be printed to the console. - -### Detecting Celebrities - -To detect celebrities you can pass in `IdentifyActionType.DETECT_CELEBRITIES`. Results are mapped to `IdentifyCelebritiesResult`. For example: - - - - -```java -public void detectCelebs(Bitmap image) { - Amplify.Predictions.identify( - IdentifyActionType.DETECT_CELEBRITIES, - image, - result -> { - IdentifyCelebritiesResult identifyResult = (IdentifyCelebritiesResult) result; - CelebrityDetails metadata = identifyResult.getCelebrities().get(0); - Log.i("MyAmplifyApp", metadata.getCelebrity().getName()); - }, - error -> Log.e("MyAmplifyApp", "Identify failed", error) - ); -} -``` - - - - -```kotlin -fun detectCelebs(image: Bitmap) { - Amplify.Predictions.identify(IdentifyActionType.DETECT_CELEBRITIES, image, - { result -> - val identifyResult = result as IdentifyCelebritiesResult - val metadata = identifyResult.celebrities.firstOrNull() - Log.i("MyAmplifyApp", "${metadata?.celebrity?.name}") - }, - { Log.e("MyAmplifyApp", "Identify failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectCelebs(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(IdentifyActionType.DETECT_CELEBRITIES, image) - val identifyResult = result as IdentifyCelebritiesResult - val celebrityName = identifyResult.celebrities.firstOrNull()?.celebrity?.name - Log.i("MyAmplifyApp", "$celebrityName") - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Identify failed", error) - } -} -``` - - - - -```java -public void detectCelebs(Bitmap image) { - RxAmplify.Predictions.identify(IdentifyActionType.DETECT_CELEBRITIES, image) - .subscribe( - result -> { - IdentifyCelebritiesResult identifyResult = (IdentifyCelebritiesResult) result; - CelebrityDetails metadata = identifyResult.getCelebrities().get(0); - Log.i("MyAmplifyApp", metadata.getCelebrity().getName()); - }, - error -> Log.e("MyAmplifyApp", "Identify failed", error) - ); -} -``` - - - - -As a result of passing in an image, `identify` will return the name of a detected celebrity. - - - - - -In order to match entities from a pre-created [Amazon Rekognition Collection](https://docs.aws.amazon.com/rekognition/latest/dg/collections.html), ensure that both `collectionId` and `maxEntities` are set in your `amplify_outputs.json` file. The value of `collectionId` should be the name of your collection that you created either with the CLI or the SDK. The value of `maxEntities` should be a number greater than `0` or less than `51` (50 is the max number of entities Rekognition can detect from a collection). If both `collectionId` and `maxEntities` do not have valid values in the `amplify_outputs.json` file, then this call will just detect entities in general with facial features, landmarks, etc. Bounding boxes for entities are returned as ratios so make sure if you would like to place the bounding box of your entity on an image that you multiple the x by the width of the image, the y by the height of the image, and both height and width ratios by the image's respective height and width. - -You can identify entity matches from your Rekognition Collection in your app using the following code sample: - - - - - -```swift -func detectEntities(_ image: URL) async throws -> [Predictions.Entity] { - do { - let result = try await Amplify.Predictions.identify(.entities, in: image) - print("Identified entities: \(result.entities)") - return result.entities - } catch let error as PredictionsError { - print("Error identifying entities: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } - } -} -``` - - - - - -```swift -func detectEntities(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify(.entities, in: image) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying entities: \(error)") - } - }, receiveValue: { value in - print("Identified entities: \(value.entities)") - }) -} -``` - - - - - -### Detecting Celebrities - -To detect celebrities you can pass in `.detectCelebrity` in the `type:` field. Results are mapped to `IdentifyCelebritiesResult`. For example: - - - - - -```swift -func detectCelebrities(_ image: URL) async throws -> [Predictions.Celebrity] { - do { - let result = try await Amplify.Predictions.identify(.celebrities, in: image) - let celebrities = result.celebrities - let celebritiesNames = celebrities.map(\.metadata.name) - print("Identified celebrities with names: \(celebritiesNames)") - return celebrities - } catch let error as PredictionsError { - print("Error identifying celebrities: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} -``` - - - - - -```swift -func detectCelebrities(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify(.celebrities, in: image) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying celebrities: \(error)") - } - }, receiveValue: { value in - print("Identified celebrities with names: \(value.celebrities.map(\.metadata.name))") - }) -} -``` - - - - - - - - - - `Predictions.identify({entities: {...}}) => Promise<>` Detects entities from an image and potentially related information such as position, faces, and landmarks. Can also identify celebrities and entities that were previously added. This function returns a Promise that returns an object with the entities that was identified. @@ -304,7 +41,7 @@ Input can be sent directly from the browser (using File object or ArrayBuffer ob Detect entities directly from image uploaded from the browser. (File object) -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -314,13 +51,12 @@ const response = await Predictions.identify({ }, } }); -console.log({ response }); ``` Detect entities directly from image binary from the browser. (ArrayBuffer object) This technique is useful when you have base64 encoded binary image data, for example, from a webcam source. -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -330,11 +66,11 @@ const response = await Predictions.identify({ }, } }); -console.log({ response }); ``` From Amazon S3 key -```javascript + +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -345,14 +81,13 @@ const response = await Predictions.identify({ }, } }); -console.log({ response }); ``` The following options are independent of which `source` is specified. For demonstration purposes it will be used `file` but it can be used S3 Key as well. Detecting bounding box of faces from an image with its landmarks (eyes, mouth, nose). -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const { entities } = await Predictions.identify({ @@ -370,7 +105,7 @@ for (const { boundingBox, landmarks } of entities) { top // top coordinate as a ratio of overall image height } = boundingBox; - for(const landmark of landmarks) { + for (const landmark of landmarks) { const { type, // string "eyeLeft", "eyeRight", "mouthLeft", "mouthRight", "nose" x, // ratio of overall image width @@ -382,7 +117,7 @@ for (const { boundingBox, landmarks } of entities) { Detecting celebrities on an image. It will return only celebrities the name and urls with related information. -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const { entities } = await Predictions.identify({ @@ -394,7 +129,7 @@ const { entities } = await Predictions.identify({ } }) -for(const { boundingBox, landmarks, metadata } of entities) { +for (const { boundingBox, landmarks, metadata } of entities) { const { name, urls @@ -407,7 +142,7 @@ for(const { boundingBox, landmarks, metadata } of entities) { Detecting entities from previously uploaded images (e.g. Advanced Configuration for Identify Entities) -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const { entities } = await Predictions.identify({ @@ -419,7 +154,7 @@ const { entities } = await Predictions.identify({ } }) -for({ boundingBox, metadata } of entities) { +for (const { boundingBox, metadata } of entities) { const { width, // ratio of overall image width height, // ratio of overall image height @@ -429,4 +164,3 @@ for({ boundingBox, metadata } of entities) { const { externalImageId } = metadata; // this is the object key on S3 from the original image } ``` - diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-text/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-text/index.mdx index 06b34082809..2e443d3430b 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-text/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/identify-text/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Identify text', description: 'Learn how to identify text from images and documents in your application using AWS Amplify.', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -34,283 +33,9 @@ export function getStaticProps(context) { ## Working with the API - - -The following APIs will allow you to identify text (words, tables, pages from a book) from an image. - -### Detect text in an image - -Amplify will make calls to both Amazon Textract and Amazon Rekognition depending on the type of text you are looking to identify (i.e. image or document). - -Passing in `TextFormatType.PLAIN` as the identify action will yield `IdentifyResult`, which must be cast into `IdentifyTextResult`. See below for an example of plain text detection from an image. - - - - -```java -public void detectText(Bitmap image) { - Amplify.Predictions.identify( - TextFormatType.PLAIN, - image, - result -> { - IdentifyTextResult identifyResult = (IdentifyTextResult) result; - Log.i("MyAmplifyApp", identifyResult.getFullText()); - }, - error -> Log.e("MyAmplifyApp", "Identify text failed", error) - ); -} -``` - - - - -```kotlin -fun detectText(image: Bitmap) { - Amplify.Predictions.identify(TextFormatType.PLAIN, image, - { result -> - val identifyResult = result as IdentifyTextResult - Log.i("MyAmplifyApp", identifyResult.fullText) - }, - { Log.e("MyAmplifyApp", "Identify text failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectText(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(PLAIN, image) - val identifyResult = result as IdentifyTextResult - Log.i("MyAmplifyApp", identifyResult.fullText) - } catch (error: PredictionsResult) { - Log.e("MyAmplifyApp", "Identify text failed", error) - } -} -``` - - - - -```java -public void detectText(Bitmap image) { - RxAmplify.Predictions.identify(TextFormatType.PLAIN, image) - .subscribe( - result -> { - IdentifyTextResult identifyResult = (IdentifyTextResult) result; - Log.i("MyAmplifyApp", identifyResult.getFullText()); - }, - error -> Log.e("MyAmplifyApp", "Identify text failed", error) - ); -} -``` - - - - -### Detect text in a document - -Passing in any other format of `TextFormatType` (i.e. `FORM`, `TABLE` or `ALL`) will yield `IdentifyResult`, which must be cast into `IdentifyDocumentTextResult`. See below for an example with `TextFormatType.FORM` for detecting forms from a document. - - - - -```java -public void detectText(Bitmap image) { - Amplify.Predictions.identify( - TextFormatType.FORM, - image, - result -> { - IdentifyDocumentTextResult identifyResult = (IdentifyDocumentTextResult) result; - Log.i("MyAmplifyApp", identifyResult.getFullText()); - }, - error -> Log.e("MyAmplifyApp", "Identify failed", error) - ); -} -``` - - - - -```kotlin -fun detectText(image: Bitmap) { - Amplify.Predictions.identify(TextFormatType.FORM, image, - { result -> - val identifyResult = result as IdentifyDocumentTextResult - Log.i("MyAmplifyApp", identifyResult.fullText) - }, - { Log.e("MyAmplifyApp", "Identify failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectText(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(FORM, image) - val identifyResult = result as IdentifyDocumentTextResult - Log.i("MyAmplifyApp", identifyResult.fullText) - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Identify failed", error) - } -} -``` - - - - -```java -public void detectText(Bitmap image) { - RxAmplify.Predictions.identify(TextFormatType.FORM, image) - .subscribe( - result -> { - IdentifyDocumentTextResult identifyResult = (IdentifyDocumentTextResult) result; - Log.i("MyAmplifyApp", identifyResult.getFullText()); - }, - error -> Log.e("MyAmplifyApp", "Identify failed", error) - ); -} -``` - - - - - - - - - -The following APIs will allow you to identify text (words, tables, pages from a book) from an image. - -For identifying text on iOS we use both AWS backend services as well as Apple's on-device Core ML [Vision Framework](https://developer.apple.com/documentation/vision) to provide you with the most accurate results. If your device is offline, we will return results only from Core ML. On the other hand, if you are able to connect to AWS Services, we will return a unioned result from both the service and Core ML. Switching between backend services and Core ML is done automatically without any additional configuration required. - -## Identify text from image - -Amplify will make calls to both Amazon Textract and Rekognition depending on the type of text you are looking to identify (i.e. image or document). - -If you are detecting text from an image you would send in `.plain` as your text format as shown below. Using `.plain` with `PredictionsIdentifyRequest.Options()` combines results from on device results from Core ML and AWS services to yield more accurate results. - - - - - -```swift -func detectText(_ image: URL) async throws -> [Predictions.IdentifiedWord]? { - do { - let result = try await Amplify.Predictions.identify(.text, in: image) - print("Identified text: \(result)") - return result.words - } catch let error as PredictionsError { - print("Error identifying text: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } - } -} -``` - - - - - -```swift -func detectText(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify(.text, in: image) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying text: \(error)") - } - }, receiveValue: { value in - print("Identified text: \(value)") - }) -} -``` - - - - - - -Bounding boxes in IdentifyTextResult are returned as ratios. If you would like to place bounding boxes on individual recognized words that appear in the image, use the following method to calculate a frame for a single bounding box. -Additionally it's important to note that Rekognition places (0,0) at the top left and Core ML places (0,0) at the bottom left. In order to handle this issue, we have flipped the y axis of the CoreML bounding box for you since iOS starts (0,0) from the top left. - - - - - - -To get results that utilize on-device capabilities (Core ML), without combining results from the backend, you can use the following to pass into the `options` argument of the `Amplify.Predictions.identify` function. -```swift -let options = Predictions.Identify.Options(defaultNetworkPolicy: .offline) -``` - -## Identify text in a document - -Sending in `.form` or `.table` or `.all` will do document analysis as well as text detection to detect tables and forms in a document. See below for an example with `.form`. - - - - - -```swift -func detectDocumentText(_ image: URL) async throws -> Predictions.Identify.DocumentText.Result { - do { - let result = try await Amplify.Predictions.identify( - .textInDocument(textFormatType: .form), in: image - ) - print("Identified document text: \(result)") - return result - } catch let error as PredictionsError { - print("Error identifying text in document: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} -``` - - - - - -```swift -func detectDocumentText(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify( - .textInDocument(textFormatType: .form), in: image - ) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying text in document: \(error)") - } - }, receiveValue: { value in - print("Identified text in document: \(value)") - }) -} -``` - - - - - - - - - Detect text in an input image. Input can be sent directly from the browser or an Amazon S3 key from project bucket. -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -320,23 +45,20 @@ const response = await Predictions.identify({ } } }); -console.log({ response }); ``` ## Identify image stored in Amazon S3 -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ text: { source: { key: pathToPhoto, - level?: 'guest' | 'private' | 'protected', //optional, default is configured on Storage category } } }) -console.log({ response }); ``` > The following options are independent of which `source` is specified. For demonstration purposes we will reference a `file` but it can be an S3 Key as well. `Predictions.identify({text : {...}})` can detect unstructured text `PLAIN`, structured text from tables `TABLE` or text from forms `FORM`. @@ -345,7 +67,7 @@ console.log({ response }); For detecting plain text, you can see the whole detected text, the lines detected, the position of each line of text, and each word. -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -380,7 +102,7 @@ const { For detecting structured forms (documents, tables, etc.) from an image, `keyValues` will return a string of the entity found in the image as well as metadata such as selected checkboxes or the relative location in the image using a `boundingBox`. -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -408,7 +130,7 @@ For example the below image would return `keyValues` with "Test" or "Checked" as For detecting structured tables from an image -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; const response = await Predictions.identify({ @@ -436,10 +158,10 @@ const { For detecting tables and forms on the image just select format "ALL" -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; -const response = await Predictions.identify({ +const { text } = await Predictions.identify({ text: { source: { file @@ -447,11 +169,4 @@ const response = await Predictions.identify({ format: 'ALL' } }); - -const { - text: { - // same as PLAIN + FORM + TABLE - } -} = response; ``` - diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/index.mdx index d5b91dc94aa..7c7879179a3 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/index.mdx @@ -6,19 +6,18 @@ export const meta = { description: 'Learn how to set up AI/ML Predictions', route: "/[platform]/build-a-backend/add-aws-services/predictions", platforms: [ - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', + 'react-native', 'vue', - 'swift', - 'android', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} export function getStaticProps(context) { const childPageNodes = getChildPageNodes(meta.route); @@ -32,42 +31,21 @@ export function getStaticProps(context) { Amplify provides provides a solution for using AI and ML cloud services to enhance your application. Some supported use cases: -
    -
  • -Convert text to speech -
  • -
  • -Transcribe audio to text -
  • -
  • -Translate text from one language to another -
  • -
  • -Identify text from an image -
  • -
  • -Identify entities from an image -
  • -
  • -Identify real world objects from an image -
  • -
  • -Interpret text -
  • -
+ +- [Convert text to speech](/[platform]/build-a-backend/add-aws-services/predictions/text-to-speech/) +- [Transcribe audio to text](/[platform]/build-a-backend/add-aws-services/predictions/transcribe-audio/) +- [Translate text from one language to another](/[platform]/build-a-backend/add-aws-services/predictions/translate/) +- [Identify text from an image](/[platform]/build-a-backend/add-aws-services/predictions/identify-text/) +- [Identify entities from an image](/[platform]/build-a-backend/add-aws-services/predictions/identify-entity/) +- [Identify real world objects from an image](/[platform]/build-a-backend/add-aws-services/predictions/identify-entity) +- [Interpret text](/[platform]/build-a-backend/add-aws-services/predictions/interpret-sentiment) Predictions is broadly organized into 3 key use cases - Identify, Convert, and Interpret - which are available in the client API as well as CLI workflows. -
    -
  • -`Identify` will find text (words, tables, pages from a book), entities (faces and/or celebrities) from images. You can also identify real world landmarks or objects such as chairs, desks, etc. which are referred to as “labels” from images. -
  • -
  • -`Convert` allows you to translate text from one source language to a target language. You can also generate speech audio from text input. Lastly, you can take an audio input and transcribe it using a websocket stream. -
  • -
  • -`Interpret` allows you to analyze text for language, entities (places, people), key phrases, sentiment (positive, neutral, negative), and syntax (pronouns, verbs, adjectives). -
  • -
+ +- `Identify` will find text (words, tables, pages from a book), entities (faces and/or celebrities) from images. You can also identify real world landmarks or objects such as chairs, desks, etc. which are referred to as “labels” from images. +- `Convert` allows you to translate text from one source language to a target language. You can also generate speech audio from text input. Lastly, you can take an audio input and transcribe it using a websocket stream. +- `Interpret` allows you to analyze text for language, entities (places, people), key phrases, sentiment (positive, neutral, negative), and syntax (pronouns, verbs, adjectives). + Some common use cases are listed below, as well as an advanced workflow which allows you to perform dynamic image indexing from a connected s3 bucket. Predictions comes with built-in support for [Amazon Translate](https://docs.aws.amazon.com/translate/latest/dg/what-is.html), [Amazon Polly](https://docs.aws.amazon.com/polly/latest/dg/what-is.html), [Amazon Transcribe](https://docs.aws.amazon.com/transcribe/latest/dg/what-is-transcribe.html), [Amazon Rekognition](https://docs.aws.amazon.com/rekognition/latest/dg/what-is.html), [Amazon Textract](https://docs.aws.amazon.com/textract/latest/dg/what-is.html), and [Amazon Comprehend](https://docs.aws.amazon.com/comprehend/latest/dg/what-is.html). diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/interpret-sentiment/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/interpret-sentiment/index.mdx index 4046f6c3846..dd5ebcf81d6 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/interpret-sentiment/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/interpret-sentiment/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Interpret sentiment', description: 'Learn how to determine key phrases, sentiment, language, syntax, and entities from text using Amplify.', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -36,120 +35,7 @@ export function getStaticProps(context) { Analyze text to find key phrases, sentiment (positive, negative, neutral), or the syntax (pronouns, verbs, etc.). You can also find entities in the text such as names or places, or perform language detection. - - -Here is an example of sending text for interpretation such as sentiment analysis or natural language characteristics. - - - - -```java -Amplify.Predictions.interpret( - "I like to eat spaghetti", - result -> Log.i("MyAmplifyApp", result.getSentiment().getValue().toString()), - error -> Log.e("MyAmplifyApp", "Interpret failed", error) -); -``` - - - - -```kotlin -Amplify.Predictions.interpret("I like to eat spaghetti", - { Log.i("MyAmplifyApp", "${it.sentiment?.value}") }, - { Log.e("MyAmplifyApp", "Interpret failed", it) } -) -``` - - - - -```kotlin -val text = "I like to eat spaghetti" -try { - val result = Amplify.Predictions.interpret(text) - Log.i("MyAmplifyApp", "${result.sentiment?.value}") -} catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Interpret failed", error) -} -``` - - - - -```java -RxAmplify.Predictions.interpret("I like to eat spaghetti") - .subscribe( - result -> Log.i("MyAmplifyApp", result.getSentiment().getValue().toString()), - error -> Log.e("MyAmplifyApp", "Interpret failed", error) - ); -``` - - - - -As a result of running this code, you will see the sentiment of the text printed to the console. - -```console -I/MyAmplifyApp: POSITIVE -``` - - - - - -For analyzing language on iOS we use both AWS backend services as well as Apple's on-device [Natural Language Framework](https://developer.apple.com/documentation/naturallanguage) to provide you with the most accurate results. If your device is offline, we will return results only Natural Language. On the other hand, if you are able to connect to AWS Services, we will return a unioned result from both the service and Natural Language. Switching between backend services and Natural Language is done automatically without any additional configuration required. - - - - - -```swift -func interpret(text: String) async throws -> Predictions.Interpret.Result { - do { - let result = try await Amplify.Predictions.interpret(text: text) - print("Interpreted text: \(result)") - return result - } catch let error as PredictionsError { - print("Error interpreting text: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} -``` - - - - - -```swift -func interpret(text: String) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.interpret(text: text) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error interpreting text: \(error)") - } - }, receiveValue: { value in - print("Interpreted text: \(value)") - }) -} -``` - - - - - - - - - - - -```js +```ts import { Predictions } from '@aws-amplify/predictions'; const result = await Predictions.interpret({ @@ -160,7 +46,4 @@ const result = await Predictions.interpret({ type: 'ALL' } }) -console.log({ result }); ``` - - diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/label-image/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/label-image/index.mdx index 25ad44b7afa..74eb6ac4637 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/label-image/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/label-image/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Label objects in an image', description: 'Learn more about how to detect labels in an image using Amplify. For example you can detect if an image has objects such as chairs, desks etc.', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -36,218 +35,6 @@ export function getStaticProps(context) { Detect labels, such if an image has a desk or a chair in it - - -### Label objects in an image - -You can identify real world objects such as chairs, desks, etc. which are referred to as “labels” by passing in `LabelType.LABELS` as the identify action. For example: - - - - -```java -public void detectLabels(Bitmap image) { - Amplify.Predictions.identify( - LabelType.LABELS, - image, - result -> { - IdentifyLabelsResult identifyResult = (IdentifyLabelsResult) result; - Label label = identifyResult.getLabels().get(0); - Log.i("MyAmplifyApp", label.getName()); - }, - error -> Log.e("MyAmplifyApp", "Label detection failed", error) - ); -} -``` - - - - -```kotlin -fun detectLabels(image: Bitmap) { - Amplify.Predictions.identify(LabelType.LABELS, image, - { result -> - val identifyResult = result as IdentifyLabelsResult - val label = identifyResult.labels.firstOrNull() - Log.i("MyAmplifyApp", "${label?.name}") - }, - { Log.e("MyAmplifyApp", "Label detection failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectLabels(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(LABELS, image) - val identifyResult = result as IdentifyLabelsResult - Log.i("MyAmplifyApp", "${identifyResult.labels[0].name}") - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Label detection failed", error) - } -} -``` - - - - -```java -public void detectLabels(Bitmap image) { - RxAmplify.Predictions.identify(LabelType.LABELS, image) - .subscribe( - result -> { - IdentifyLabelsResult identifyResult = (IdentifyLabelsResult) result; - Label label = identifyResult.getLabels().get(0); - Log.i("MyAmplifyApp", label.getName()); - }, - error -> Log.e("MyAmplifyApp", "Label detection failed", error) - ); -} -``` - - - - -### Label moderation tag in an image - -You can also detect whether identified content inside the image is safe by enabling moderation by passing in `LabelType.MODERATION_LABELS`. - - - - -```java -public void detectLabels(Bitmap image) { - Amplify.Predictions.identify( - LabelType.MODERATION_LABELS, - image, - result -> { - IdentifyLabelsResult identifyResult = (IdentifyLabelsResult) result; - Log.i("MyAmplifyApp", Boolean.toString(identifyResult.isUnsafeContent())); - }, - error -> Log.e("MyAmplifyApp", "Identify failed", error) - ); -} -``` - - - - -```kotlin -fun detectLabels(image: Bitmap) { - Amplify.Predictions.identify(LabelType.MODERATION_LABELS, image, - { result -> - val identifyResult = result as IdentifyLabelsResult - Log.i("MyAmplifyApp", "${identifyResult.isUnsafeContent}") - }, - { Log.e("MyAmplifyApp", "Identify failed", it) } - ) -} -``` - - - - -```kotlin -suspend fun detectLabels(image: Bitmap) { - try { - val result = Amplify.Predictions.identify(MODERATION_LABELS, image) - val identifyResult = result as IdentifyLabelsResult - Log.i("MyAmplifyApp", identifyResult.isUnsafeContent) - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Identify failed", error) - } -} -``` - - - - - - - - - -For labeling images on iOS we use both AWS backend services as well as Apple's on-device Core ML [Vision Framework](https://developer.apple.com/documentation/vision) to provide you with the most accurate results. If your device is offline, we will return results only from Core ML. On the other hand, if you are able to connect to AWS Services, we will return a unioned result from both the service and Core ML. Switching between backend services and Core ML is done automatically without any additional configuration required. - - - - - -```swift -func detectLabels(_ image: URL) async throws -> Predictions.Identify.Labels.Result { - do { - let result = try await Amplify.Predictions.identify(.labels(type: .labels), in: image) - print("Identified labels: \(result.labels)") - return result - } catch let error as PredictionsError { - print("Error identifying labels: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} - -// To identify labels with unsafe content -func detectAllLabels(_ image: URL) async throws -> Predictions.Identify.Labels.Result { - do { - let result = try await Amplify.Predictions.identify(.labels(type: .all), in: image) - print("Identified labels: \(result.labels)") - return result - } catch let error as PredictionsError { - print("Error identifying labels: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} -``` - - - - - -```swift -func detectLabels(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify(.labels(type: .labels), in: image) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying labels: \(error)") - } - }, receiveValue: { value in - print("Identified labels: \(labels)") - }) -} - -// To identify labels with unsafe content -func detectAllLabels(_ image: URL) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.identify(.labels(type: .all), in: image) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error identifying labels: \(error)") - } - }, receiveValue: { value in - print("Identified labels: \(labels)") - }) -} -``` - - - - - - - - - ```javascript import { Predictions } from '@aws-amplify/predictions'; @@ -270,10 +57,10 @@ Predictions.identify({ Detect unsafe content in an image -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; -Predictions.identify({ +const { unsafe } = await Predictions.identify({ labels: { source: { file @@ -281,18 +68,14 @@ Predictions.identify({ type: 'UNSAFE' } }) - .then((response) => { - const { unsafe } = response; // boolean - }) - .catch((err) => console.log({ err })); ``` -for both labels and unsafe content +For both labels and unsafe content -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; -Predictions.identify({ +const { labels, unsafe } = await Predictions.identify({ labels: { source: { file @@ -300,14 +83,4 @@ Predictions.identify({ type: 'ALL' } }) - .then((response) => { - const { labels } = response; - const { unsafe } = response; // boolean - labels.forEach((object) => { - const { name, boundingBoxes } = object; - }); - }) - .catch((err) => console.log({ err })); ``` - - 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 72b39b4bd33..2b5dba20159 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 @@ -4,21 +4,20 @@ export const meta = { title: 'Set up Predictions', description: 'Get started with integrating ML capabilities into your application using Amplify', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -26,427 +25,7 @@ export function getStaticProps(context) { }; } - - -**Under active development:** The `addOutput` method for Amplify Gen 2 is under active development. The experience may change between versions of `@aws-amplify/backend`. Try it out and provide feedback at https://github.com/aws-amplify/amplify-backend/issues/new/choose - - - - - -## Prerequisites - - -* An Android application targeting Android API level 24 (Android 7.0) or above - * For a full example, please follow the [mobile support walkthrough](/[platform]/start/quickstart/) . - - - - - - - -On iOS, we leverage Apple’s Core ML [Vision Framework](https://developer.apple.com/documentation/vision) and [Natural Language Framework](https://developer.apple.com/documentation/naturallanguage) to improve accuracy as well as support cases where your device is unable to reach AWS Services. For more information, see each individual use case. - -## Prerequisites - - -An application with Amplify libraries integrated and a minimum target of any of the following: -- **iOS 13.0**, using **Xcode 14.1** or later. -- **macOS 10.15**, using **Xcode 14.1** or later. -- **tvOS 13.0**, using **Xcode 14.3** or later. -- **watchOS 9.0**, using **Xcode 14.3** or later. -- **visionOS 1.0**, using **Xcode 15 beta 2** or later. (Preview support - see below for more details.) - -For a full example, please follow the [mobile support walkthrough](/swift/start/quickstart/) . - - - -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. -As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. - -For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). - - - - - -### Set up the backend - -To enable Predictions we need to set up the appropriate IAM policy for Roles in your Amazon Cognito Identity Pool in order to use an appropriate feature. Additionally, we need to use the ```addOutput``` method to patch the custom Predictions resource to the expected output configuration. - - - -**Note:** In the following example, we configure the policy to enable all supported ML capabilities. Ensure to include only the actions & resources relevant to your specific use cases. -To learn more, check the docs of [Amazon Translate](https://docs.aws.amazon.com/translate/latest/dg/what-is.html), [Amazon Polly](https://docs.aws.amazon.com/polly/latest/dg/what-is.html), [Amazon Transcribe](https://docs.aws.amazon.com/transcribe/latest/dg/what-is-transcribe.html), [Amazon Rekognition](https://docs.aws.amazon.com/rekognition/latest/dg/what-is.html), [Amazon Textract](https://docs.aws.amazon.com/textract/latest/dg/what-is.html), and [Amazon Comprehend](https://docs.aws.amazon.com/comprehend/latest/dg/what-is.html). - - - -```ts title="amplify/backend.ts" - -import { defineBackend } from "@aws-amplify/backend"; -import { auth } from "./auth/resource"; -import { Stack } from "aws-cdk-lib"; -import { PolicyStatement } from "aws-cdk-lib/aws-iam"; - -const backend = defineBackend({ - auth, -}); - - -// Configure a policy for the required use case. -// The actions included below cover all supported ML capabilities -backend.auth.resources.unauthenticatedUserIamRole.addToPrincipalPolicy( - new PolicyStatement({ - actions: [ - "translate:TranslateText", - "transcribe:StartStreamTranscriptionWebSocket", - "polly:SynthesizeSpeech", - "comprehend:DetectSentiment", - "comprehend:DetectEntities", - "comprehend:DetectDominantLanguage", - "comprehend:DetectSyntax", - "comprehend:DetectKeyPhrases", - "rekognition:DetectFaces", - "rekognition:RecognizeCelebrities", - "rekognition:DetectLabels", - "rekognition:DetectModerationLabels", - "rekognition:DetectText", - "rekognition:DetectLabel", - "rekognition:SearchFacesByImage", - "textract:AnalyzeDocument", - "textract:DetectDocumentText", - "textract:GetDocumentAnalysis", - "textract:StartDocumentAnalysis", - "textract:StartDocumentTextDetection", - ], - resources: ["*"], - }) -); - -backend.addOutput({ - custom: { - Predictions: { - convert: { - translateText: { - defaults: { - sourceLanguage: "en", - targetLanguage: "es", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - speechGenerator: { - defaults: { - voiceId: "Ivy", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - transcription: { - defaults: { - language: "en-US", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - }, - identify: { - identifyEntities: { - defaults: { - collectionId: "default", - maxEntities: 10, - }, - celebrityDetectionEnabled: true, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - identifyLabels: { - defaults: { - type: "ALL", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - identifyText: { - defaults: { - format: "ALL", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - }, - interpret: { - interpretText: { - defaults: { - type: "ALL", - }, - proxy: false, - region: Stack.of(backend.auth.resources.unauthenticatedUserIamRole) - .region, - }, - }, - }, - }, -}); - - - -``` - -## Install Amplify Libraries - - - -## Prerequisites - - -Expand **Gradle Scripts**, open **build.gradle (Module :app)**. You will already have configured Amplify by following the steps in the [mobile support walkthrough](/[platform]/start/quickstart/). - -Add Predictions by adding these libraries into the `dependencies` block: - -```groovy -dependencies { - // Add these lines in `dependencies` - implementation 'com.amplifyframework:aws-predictions:ANDROID_VERSION' - implementation 'com.amplifyframework:aws-auth-cognito:ANDROID_VERSION' -} -``` - -`aws-auth-cognito` provides authentication for the backend services used by `aws-predictions`. - -Click **Sync Now**. - -## Initialize Amplify Predictions - -To initialize the Amplify Predictions and Authentication categories you call `Amplify.addPlugin()` method for each category. To complete initialization call `Amplify.configure()`. - -Add the following code to your `onCreate()` method in your application class: - - - - -```java -import android.util.Log; -import com.amplifyframework.AmplifyException; -import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; -import com.amplifyframework.core.Amplify; -import com.amplifyframework.predictions.aws.AWSPredictionsPlugin; -``` - -```java -Amplify.addPlugin(new AWSCognitoAuthPlugin()); -Amplify.addPlugin(new AWSPredictionsPlugin()); -``` - -Your class will look like this: - -```java -public class MyAmplifyApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - try { - // Add these lines to add the AWSCognitoAuthPlugin and AWSPredictionsPlugin plugins - Amplify.addPlugin(new AWSCognitoAuthPlugin()); - Amplify.addPlugin(new AWSPredictionsPlugin()); - Amplify.configure(getApplicationContext()); - - Log.i("MyAmplifyApp", "Initialized Amplify"); - } catch (AmplifyException error) { - Log.e("MyAmplifyApp", "Could not initialize Amplify", error); - } - } -} -``` - - - - -```kotlin -import android.util.Log -import com.amplifyframework.AmplifyException -import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin -import com.amplifyframework.core.Amplify -import com.amplifyframework.predictions.aws.AWSPredictionsPlugin -``` - -```kotlin -Amplify.addPlugin(AWSCognitoAuthPlugin()) -Amplify.addPlugin(AWSPredictionsPlugin()) -``` - -Your class will look like this: - -```kotlin -class MyAmplifyApp : Application() { - override fun onCreate() { - super.onCreate() - - try { - // Add these lines to add the AWSCognitoAuthPlugin and AWSPredictionsPlugin plugins - Amplify.addPlugin(AWSCognitoAuthPlugin()) - Amplify.addPlugin(AWSPredictionsPlugin()) - Amplify.configure(applicationContext) - - Log.i("MyAmplifyApp", "Initialized Amplify") - } catch (error: AmplifyException) { - Log.e("MyAmplifyApp", "Could not initialize Amplify", error) - } - } -} -``` - - - - -```java -import android.util.Log; -import com.amplifyframework.AmplifyException; -import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; -import com.amplifyframework.predictions.aws.AWSPredictionsPlugin; -import com.amplifyframework.rx.RxAmplify; -``` - -```java -RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); -RxAmplify.addPlugin(new AWSPredictionsPlugin()); -``` - -Your class will look like this: - -```java -public class MyAmplifyApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - try { - // Add these lines to add the AWSCognitoAuthPlugin and AWSPredictionsPlugin plugins - RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); - RxAmplify.addPlugin(new AWSPredictionsPlugin()); - RxAmplify.configure(getApplicationContext()); - - Log.i("MyAmplifyApp", "Initialized Amplify"); - } catch (AmplifyException error) { - Log.e("MyAmplifyApp", "Could not initialize Amplify", error); - } - } -} -``` - - - - -Note that because the predictions category requires [auth](/[platform]/build-a-backend/auth/set-up-auth/). - - - - - - - -1. To install Amplify Libraries in your application, open your project in Xcode and select **File > Add Packages...**. - -2. Enter the **Amplify Library for Swift** GitHub repo URL (`https://github.com/aws-amplify/amplify-swift`) into the search bar and click **Add Package**. - - - - **Note:** **Up to Next Major Version** should be selected from the **Dependency Rule** dropdown. - - - -3. Lastly, choose **Amplify** and **AWSPredictionsPlugin**. Then click **Add Package**. - -## Initialize Amplify Predictions - -To initialize the Amplify Predictions and Auth categories, use the `Amplify.add(plugin:)` method to add **AWSCognitoAuthPlugin** and **AWSPredictionsPlugin**. Next finish configuring Amplify by calling `Amplify.configure()`. - -Open the main file of your application - `AppDelegate.swift` or `App.swift` - and **add the following import statements** to the top of the file. -```swift -import Amplify -import AWSCognitoAuthPlugin -import AWSPredictionsPlugin -``` - -**Configure Amplify at app launch** - - - - - -```swift -@main -struct MyAmplifyApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } - - init() { - do { - try Amplify.add(plugin: AWSCognitoAuthPlugin()) - try Amplify.add(plugin: AWSPredictionsPlugin()) - try Amplify.configure() - print("Amplify configured with Auth and Predictions plugins") - } catch { - print("Failed to initialize Amplify with \(error)") - } - } - } -} -``` - - - - - -Add to your AppDelegate's `application:didFinishLaunchingWithOptions` method - -```swift -func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? -) -> Bool { - - do { - try Amplify.add(plugin: AWSCognitoAuthPlugin()) - try Amplify.add(plugin: AWSPredictionsPlugin()) - try Amplify.configure() - print("Amplify configured with Auth and Predictions plugins") - } catch { - print("Failed to initialize Amplify with \(error)") - } - - return true -} -``` - - - - - -Upon building and running this application you should see the following in your console window: - -```console -Amplify configured with Auth and Predictions plugins -``` - - - - - - - -To install the Amplify library to use predictions features, run the following commands in your project’s root folder: +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 @@ -454,10 +33,9 @@ npm add aws-amplify ## Configure the frontend -Import and load the configuration file in your app. It's recommended you add the Amplify configuration step to your app's root entry point. For example ```index.js``` in React or ```main.ts``` in Angular. - -``` +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"; @@ -467,7 +45,4 @@ Amplify.configure({ ...Amplify.getConfig(), Predictions: config.custom.Predictions, }); - ``` - - diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/text-to-speech/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/text-to-speech/index.mdx index 41bb508313b..b1b4c5f070f 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/text-to-speech/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/text-to-speech/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Text to speech', description: 'Learn how to integrate text-to-speech capabilities into your application using Amplify.', platforms: [ - 'swift', - 'android', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -36,258 +35,10 @@ export function getStaticProps(context) { Generate an audio buffer for playback from a text input. - - - - - -Open `MainActivity.java` and add the following code: - -```java -private final MediaPlayer mp = new MediaPlayer(); - -@Override -protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - Amplify.Predictions.convertTextToSpeech( - "I like to eat spaghetti", - result -> playAudio(result.getAudioData()), - error -> Log.e("MyAmplifyApp", "Conversion failed", error) - ); -} - -private void playAudio(InputStream data) { - File mp3File = new File(getCacheDir(), "audio.mp3"); - - try (OutputStream out = new FileOutputStream(mp3File)) { - byte[] buffer = new byte[8 * 1_024]; - int bytesRead; - while ((bytesRead = data.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - mp.reset(); - mp.setOnPreparedListener(MediaPlayer::start); - mp.setDataSource(new FileInputStream(mp3File).getFD()); - mp.prepareAsync(); - } catch (IOException error) { - Log.e("MyAmplifyApp", "Error writing audio file", error); - } -} -``` - - - - -Open `MainActivity.kt` and add the following code: - -```kotlin -private val mp = MediaPlayer() - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - convertTextToSpeech() -} - -private fun convertTextToSpeech() { - Amplify.Predictions.convertTextToSpeech("I like to eat spaghetti!", - { playAudio(it.audioData) }, - { Log.e("MyAmplifyApp", "Failed to convert text to speech", it) } - ) -} - -private fun playAudio(data: InputStream) { - val mp3File = File(cacheDir, "audio.mp3") - try { - FileOutputStream(mp3File).use { out -> - val buffer = ByteArray(8 * 1024) - var bytesRead: Int - while (data.read(buffer).also { bytesRead = it } != -1) { - out.write(buffer, 0, bytesRead) - } - mp.reset() - mp.setOnPreparedListener { obj: MediaPlayer -> obj.start() } - mp.setDataSource(FileInputStream(mp3File).fd) - mp.prepareAsync() - } - } catch (error: IOException) { - Log.e("MyAmplifyApp", "Error writing audio file.") - } -} -``` - - - - -Open `MainActivity.kt` and add the following code: - -```kotlin -private val mp = MediaPlayer() - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - convertTextToSpeech() -} - -private fun convertTextToSpeech() = activityScope.launch { - val text = "I like to eat spaghetti!" - try { - val result = Amplify.Predictions.convertTextToSpeech(text) - playAudio(result.audioData) - } catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Failed to convert text to speech", error) - } -} - -private fun playAudio(data: InputStream) { - val mp3File = File(cacheDir, "audio.mp3") - try { - FileOutputStream(mp3File).use { out -> - val buffer = ByteArray(8 * 1024) - var bytesRead: Int - while (data.read(buffer).also { bytesRead = it } != -1) { - out.write(buffer, 0, bytesRead) - } - mp.reset() - mp.setOnPreparedListener { obj: MediaPlayer -> obj.start() } - mp.setDataSource(FileInputStream(mp3File).fd) - mp.prepareAsync() - } - } catch (error: IOException) { - Log.e("MyAmplifyApp", "Error writing audio file.") - } -} -``` - - - - -Open `MainActivity.java` and add the following code: - -```java -private final MediaPlayer mp = new MediaPlayer(); - -@Override -protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - RxAmplify.Predictions.convertTextToSpeech("I like to eat spaghetti") - .subscribe( - result -> playAudio(result.getAudioData()), - error -> Log.e("MyAmplifyApp", "Conversion failed", error) - ); -} - -private void playAudio(InputStream data) { - File mp3File = new File(getCacheDir(), "audio.mp3"); - - try (OutputStream out = new FileOutputStream(mp3File)) { - byte[] buffer = new byte[8 * 1_024]; - int bytesRead; - while ((bytesRead = data.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - mp.reset(); - mp.setOnPreparedListener(MediaPlayer::start); - mp.setDataSource(new FileInputStream(mp3File).getFD()); - mp.prepareAsync(); - } catch (IOException error) { - Log.e("MyAmplifyApp", "Error writing audio file", error); - } -} -``` - - - - -This example works on all supported versions of Android. Android API 23 added support for [`MediaDataSource`](https://developer.android.com/reference/android/media/MediaDataSource), which allows for `InputStream` from Amplify to be read directly without writing to a file. - -As a result of running this code, you will hear audio of the text being emitted from your device. - - - - - - - - - - - -```swift -import Amplify -import AWSPredictionsPlugin -import AVFoundation - -//... - -var player: AVAudioPlayer? - -//... - -func textToSpeech() async throws { - let result = try await Amplify.Predictions.convert( - .textToSpeech("Hello, world!"), - options: .init(voice: .englishFemaleIvy) - ) - print("TextToSpeech result: \(result)") - self.player = try AVAudioPlayer(data: result.audioData) - player?.play() -} -``` - - - - - -```swift -import Amplify -import AWSPredictionsPlugin -import AVFoundation - -//... - -var player: AVAudioPlayer? -var textToSpeechSink: AnyCancellable? - -//... - -func textToSpeech() { - textToSpeechSink = Amplify.Publisher.create { - try await Amplify.Predictions.convert( - .textToSpeech("Hello, world!"), - options: .init(voice: .englishFemaleIvy) - ) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error converting text to speech: \(error)") - } - }, receiveValue: { result in - print("TextToSpeech result: \(result)") - self.player = try? AVAudioPlayer(data: result.audioData) - self.player?.play() - }) -} -``` - - - - - -As a result of running this code, you will hear audio of the text being emitted from your device. - - - - -``` +```ts import { Predictions } from '@aws-amplify/predictions'; -Predictions.convert({ +const result = await Predictions.convert({ textToSpeech: { source: { text: textToGenerateSpeech @@ -295,13 +46,8 @@ Predictions.convert({ voiceId: "Amy" } }) -.then(result => console.log({ result })) -.catch(err => console.log({ err })); - ``` - - To view the complete list of voiceId options refer to [Voices in Amazon Polly](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/transcribe-audio/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/transcribe-audio/index.mdx index 93075437506..540d33cfd9c 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/transcribe-audio/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/transcribe-audio/index.mdx @@ -4,20 +4,20 @@ export const meta = { title: 'Transcribe audio to text', description: 'Learn more about how to transcribe audio to text (also known as speech-to-text) for your application using Amplify', platforms: [ - 'swift', - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', - 'vue' + 'react-native', + 'vue', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -34,42 +34,17 @@ export function getStaticProps(context) { ## Working with the API You can transcribe a PCM Audio byte buffer to Text, such as a recording from microphone. - - -```swift -func speechToText(url: URL) async throws { - let options = Predictions.Convert.SpeechToText.Options( - defaultNetworkPolicy: .auto, - language: .usEnglish - ) - - let result = try await Amplify.Predictions.convert( - .speechToText(url: url), options: options - ) - - let transcription = result.map(\.transcription) - for try await transcriptionPart in transcription { - print("transcription part: \(transcriptionPart)") - } -} -``` - - - - -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; -Predictions.convert({ +const { transcription } = await Predictions.convert({ transcription: { source: { bytes } } }) -.then(({ transcription: { fullText } }) => console.log({ fullText })) -.catch((err) => console.log({ err })); ``` - + To view the complete list of all the supported languages and language specific features refer to [the supported languages list](https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html). The language data input type has to support streaming for it to work with Amplify Predictions. diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/translate/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/translate/index.mdx index d426523cc62..d2fdf160cec 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/predictions/translate/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/predictions/translate/index.mdx @@ -4,21 +4,20 @@ export const meta = { title: 'Translate language', description: 'Learn more about how to integrate translation capabilities for your application using Amplify', platforms: [ - 'javascript', 'angular', + 'javascript', 'nextjs', 'react', + 'react-native', 'vue', - 'swift', - 'android', ] }; -export const getStaticPaths = async () => { +export async function getStaticPaths() { return getCustomStaticPath(meta.platforms); -}; +} -export function getStaticProps(context) { +export function getStaticProps() { return { props: { meta @@ -37,196 +36,10 @@ export function getStaticProps(context) { Translate text from one source language to a destination language. - - -### Translate text as configured - - - - -Open `MainActivity.java` and add the following to the bottom of `onCreate()`: - -```java -Amplify.Predictions.translateText("I like to eat spaghetti", - result -> Log.i("MyAmplifyApp", result.getTranslatedText()), - error -> Log.e("MyAmplifyApp", "Translation failed", error) -); -``` - - - - -Open `MainActivity.kt` and add the following to the bottom of `onCreate()`: - -```kotlin -Amplify.Predictions.translateText("I like to eat spaghetti", - { Log.i("MyAmplifyApp", it.translatedText) }, - { Log.e("MyAmplifyApp", "Translation failed", it) } -) -``` - - - - -Open `MainActivity.kt` and add the following to the bottom of `onCreate()`: - -```kotlin -val text = "I like to eat spaghetti" -try { - val result = Amplify.Predictions.translateText(text) - Log.i("MyAmplifyApp", result.translatedText) -} catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Translation failed", error) -} -``` - - - - -Open `MainActivity.java` and add the following to the bottom of `onCreate()`: - -```java -RxAmplify.Predictions.translateText("I like to eat spaghetti") - .subscribe( - result -> Log.i("MyAmplifyApp", result.getTranslatedText()), - error -> Log.e("MyAmplifyApp", "Translation failed", error) - ); -``` - - - - -As a result of running this code, you will see the translated text printed to the console. - -```console -I/MyAmplifyApp: Mi piace mangiare gli spaghetti -``` - -### Override configured language - -In order to override any choices you made in regards to target or source languages while adding this resource to the Amplify backend, you can pass them in directly as parameters as shown below. - -Add the `LanguageType` options as below: - - - - -```java -Amplify.Predictions.translateText( - "I like to eat spaghetti", LanguageType.ENGLISH, LanguageType.RUSSIAN, - result -> Log.i("MyAmplifyApp", result.getTranslatedText()), - error -> Log.e("MyAmplifyApp", "Translation failed", error) -); -``` - - - - -```kotlin -Amplify.Predictions.translateText( - "I like to eat spaghetti", LanguageType.ENGLISH, LanguageType.RUSSIAN, - { Log.i("MyAmplifyApp", it.translatedText) }, - { Log.e("MyAmplifyApp", "Translation failed", it) } -) -``` - - - - -Open `MainActivity.kt` and add the following to the bottom of `onCreate()`: - -```kotlin -val text = "I like to eat spaghetti" -try { - val result = Amplify.Predictions.translateText(text, ENGLISH, RUSSIAN) - Log.i("MyAmplifyApp", result.translatedText) -} catch (error: PredictionsException) { - Log.e("MyAmplifyApp", "Translation failed", error) -} -``` - - - - -```java -RxAmplify.Predictions.translateText("I like to eat spaghetti", - LanguageType.ENGLISH, - LanguageType.RUSSIAN) - .subscribe( - result -> Log.i("MyAmplifyApp", result.getTranslatedText()), - error -> Log.e("MyAmplifyApp", "Translation failed", error) - ); -``` - - - - -As a result of running this code, you will see the translated text printed to the console. - -```console -I/MyAmplifyApp: Мне нравится есть спагетти -``` - - - - - - - - - -```swift -func translateText(text: String) async throws -> String { - do { - let result = try await Amplify.Predictions.convert( - .translateText(text, from: .english, to: .italian) - ) - print("Translated text: \(result.text)") - return result.text - } catch let error as PredictionsError { - print("Error translating text: \(error)") - throw error - } catch { - print("Unexpected error: \(error)") - throw error - } -} -``` - - - - - -```swift -func translateText(text: String) -> AnyCancellable { - Amplify.Publisher.create { - try await Amplify.Predictions.convert( - .translateText(text, from: .english, to: .italian) - ) - } - .sink(receiveCompletion: { completion in - if case let .failure(error) = completion { - print("Error translating text: \(error)") - } - }, receiveValue: { value in - print("Translated text: \(value.text)") - }) -} -``` - - - - - -As a result of running this code, you will see the translated text printed to the console. - - - - -```javascript +```ts import { Predictions } from '@aws-amplify/predictions'; -Predictions.convert({ +const result = await Predictions.convert({ translateText: { source: { text: textToTranslate, @@ -235,12 +48,8 @@ Predictions.convert({ targetLanguage: "en" } }) -.then(result => console.log({ result })) -.catch(err => console.log({ err })); ``` - - To view the complete list of supported languages refer to [Supported languages and language codes](https://docs.aws.amazon.com/translate/latest/dg/what-is-languages.html). diff --git a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/customize-authz/index.mdx b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/customize-authz/index.mdx index 4082cb7bf5b..b60680067bf 100644 --- a/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/customize-authz/index.mdx +++ b/src/pages/[platform]/build-a-backend/add-aws-services/rest-api/customize-authz/index.mdx @@ -317,7 +317,7 @@ class MyOIDCAuthProvider : AmplifyOIDCAuthProvider { throw error } } - + throw AuthError.unknown("Could not retrieve Cognito token") } } 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 d0dce2f4cba..7710287d8de 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 @@ -34,7 +34,7 @@ To setup and configure your application with REST API or HTTP API to make reques ## Set up REST API with Lambda Function Create a new directory and a resource file, `amplify/functions/api-function/resource.ts`. Then, define the function with `defineFunction`: - + ```ts title="amplify/functions/api-function/resource.ts" import { defineFunction } from "@aws-amplify/backend"; @@ -173,7 +173,7 @@ backend.addOutput({ ## Set up HTTP API with Lambda Function Create a new directory and a resource file, `amplify/functions/api-function/resource.ts`. Then, define the function with `defineFunction`: - + ```ts title="amplify/functions/api-function/resource.ts" import { defineFunction } from "@aws-amplify/backend"; @@ -327,7 +327,7 @@ backend.addOutput({ Use the package manager of your choice to install the amplify JS library. For example, with `npm`: -```bash title="Terminal" showLineNumbers={false} +```bash title="Terminal" showLineNumbers={false} npm add aws-amplify ``` @@ -346,7 +346,7 @@ Use the package manager of your choice to install the amplify JS library. For ex -```bash title="Terminal" showLineNumbers={false} +```bash title="Terminal" showLineNumbers={false} npm add aws-amplify @aws-amplify/react-native ``` @@ -394,7 +394,7 @@ dependencies: sdk: flutter amplify_flutter: ^1.0.0 amplify_api: ^1.0.0 - amplify_auth_cognito: ^1.0.0 + amplify_auth_cognito: ^1.0.0 ```
@@ -497,7 +497,7 @@ func application( do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSAPIPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with API and Auth plugin") } catch { print("Failed to initialize Amplify with \(error)") @@ -523,6 +523,16 @@ To initialize the Amplify Auth and API categories you call `Amplify.addPlugin()` Add the following code to your `onCreate()` method in your application class: + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + @@ -543,7 +553,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the `AWSApiPlugin` and `AWSCognitoAuthPlugin` Amplify.addPlugin(new AWSApiPlugin()); Amplify.addPlugin(new AWSCognitoAuthPlugin()); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify."); } catch (AmplifyException error) { @@ -572,7 +582,7 @@ class MyAmplifyApp : Application() { // Add these lines to add the `AWSApiPlugin` and `AWSCognitoAuthPlugin` Amplify.addPlugin(AWSApiPlugin()) Amplify.addPlugin(AWSCognitoAuthPlugin()) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify.") } catch (error: AmplifyException) { @@ -602,7 +612,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the `AWSApiPlugin` and `AWSCognitoAuthPlugin` RxAmplify.addPlugin(new AWSApiPlugin()); RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify."); } catch (AmplifyException error) { 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 2e883cbce6f..e365cf82809 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 @@ -115,7 +115,7 @@ final session = await cognitoPlugin.federateToIdentityPool( ## Subscribing Events -You can take specific actions when users sign-in or sign-out by subscribing to authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/utilities/hub/) for more information. +You can take specific actions when users sign-in or sign-out by subscribing to authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/auth/connect-your-frontend/listen-to-auth-events/) for more information. ## Identity Pool Federation @@ -283,7 +283,7 @@ val options = FederateToIdentityPoolOptions.builder() ## Subscribing Events -You can take specific actions when users sign-in or sign-out by subscribing authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/utilities/hub/) for more information. +You can take specific actions when users sign-in or sign-out by subscribing authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/auth/connect-your-frontend/listen-to-auth-events/) for more information. ## Identity Pool Federation @@ -379,7 +379,7 @@ func federateToIdentityPoolsUsingCustomIdentityId() async throws { ## Subscribing to Events -You can take specific actions when users sign-in or sign-out by subscribing to authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/utilities/hub/) for more information. +You can take specific actions when users sign-in or sign-out by subscribing to authentication events in your app. Please see our [Hub Module Developer Guide](/[platform]/build-a-backend/auth/connect-your-frontend/listen-to-auth-events/) for more information. ## Identity Pool Federation diff --git a/src/pages/[platform]/build-a-backend/auth/customize-auth-lifecycle/custom-auth-flows/index.mdx b/src/pages/[platform]/build-a-backend/auth/customize-auth-lifecycle/custom-auth-flows/index.mdx index bba14684a30..5dadf1e6cd6 100644 --- a/src/pages/[platform]/build-a-backend/auth/customize-auth-lifecycle/custom-auth-flows/index.mdx +++ b/src/pages/[platform]/build-a-backend/auth/customize-auth-lifecycle/custom-auth-flows/index.mdx @@ -6,14 +6,8 @@ export const meta = { 'Use Amazon Cognito Auth plugin to sign in a user into Amazon Cognito User Pool using user defined custom flow', platforms: [ 'android', - // 'angular', 'flutter', - // 'javascript', - // 'nextjs', - // 'react', - // 'react-native', 'swift', - // 'vue' ], }; @@ -41,7 +35,7 @@ An application with Amplify libraries integrated and a minimum target of any of - **watchOS 9.0**, using **Xcode 14.3** or later. - **visionOS 1.0**, using **Xcode 15 beta 2** or later. (Preview support - see below for more details.) -For a full example, please follow the [project setup walkthrough](/[platform]/start/project-setup/prerequisites/). +For a full example, please follow the [project setup walkthrough](/[platform]/start/quickstart/). @@ -66,7 +60,7 @@ For more information on adding capabilities to your application, see [Xcode Capa The custom auth flow can be [configured manually](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). -If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your [Amplify configuration](/[platform]/build-a-backend/auth/existing-resources/) to `CUSTOM_AUTH`. +If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your Amplify configuration to `CUSTOM_AUTH`. For more information on authentication flow types, you can check out the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html). ## Register a user @@ -244,7 +238,7 @@ Since this is a custom authentication flow with a challenge, the result of the s ## Confirm sign in with custom challenge -Get the custom challenge (`1234` in this case) from the user and pass it to the `confirmSignin()` api. +To get a custom challenge from the user, create an appropriate UI for the user to submit the required value, and pass that value into the `confirmSignin()` API. @@ -294,9 +288,9 @@ Confirm sign in succeeded ### Lambda Trigger Setup -AWS Amplify now supports creating functions as part of its new backend experience. For more information on the Functions and how to start with them check out [Functions documentation](/[platform]/build-a-backend/functions/). In addition, more information on available triggers can be found in the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). +AWS Amplify now supports creating functions as part of its new backend experience. For more information on the Functions and how to start with them check out [Functions documentation](/[platform]/build-a-backend/functions/). In addition, more information on available triggers can be found in the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html). -### Custom Auth Flow with SRP +### Custom Auth Flow with Secure Remote Password (SRP) Cognito User Pool allows to start the custom authentication flow with SRP as the first step. If you would like to use this flow, setup Define Auth Lambda trigger to handle SRP_A as the first challenge as shown below: @@ -337,64 +331,6 @@ let signInResult = try await Amplify.Auth.signIn( options: .init(pluginOptions: options)) ``` -### CAPTCHA-based authentication - -Here is the sample for creating a CAPTCHA challenge with a Lambda Trigger. - -The `Create Auth Challenge Lambda Trigger` creates a CAPTCHA as a challenge to the user. The URL for the CAPTCHA image and the expected answer is added to the private challenge parameters: - -```javascript -export const handler = async (event) => { - if (!event.request.session || event.request.session.length === 0) { - event.response.publicChallengeParameters = { - captchaUrl: , - }; - event.response.privateChallengeParameters = { - answer: , - }; - event.response.challengeMetadata = "CAPTCHA_CHALLENGE"; - } - return event; -}; -``` - -This `Define Auth Challenge Lambda Trigger` defines a custom challenge: - -```javascript -export const handler = async (event) => { - if (!event.request.session || event.request.session.length === 0) { - // If we don't have a session or it is empty then send a CUSTOM_CHALLENGE - event.response.challengeName = "CUSTOM_CHALLENGE"; - event.response.failAuthentication = false; - event.response.issueTokens = false; - } else if (event.request.session.length === 1 && event.request.session[0].challengeResult === true) { - // If we passed the CUSTOM_CHALLENGE then issue token - event.response.failAuthentication = false; - event.response.issueTokens = true; - } else { - // Something is wrong. Fail authentication - event.response.failAuthentication = true; - event.response.issueTokens = false; - } - - return event; -}; -``` - -The `Verify Auth Challenge Response Lambda Trigger` is used to verify a challenge answer: - -```javascript -export const handler = async (event, context) => { - if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) { - event.response.answerCorrect = true; - } else { - event.response.answerCorrect = false; - } - - return event; -}; -``` - The Auth category can be configured to perform a [custom authentication flow](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html) defined by you. The following guide shows how to setup a simple passwordless authentication flow. @@ -402,13 +338,13 @@ The Auth category can be configured to perform a [custom authentication flow](ht ## Prerequisites * An Android application targeting at least Android SDK API level 24 with Amplify libraries integrated - * For a full example of creating Android project, please follow the [project setup walkthrough](/[platform]/start/project-setup/create-application/) + * For a full example of creating Android project, please follow the [project setup walkthrough](/[platform]/start/quickstart/) ## Configure Auth The custom auth flow can be [configured manually](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). -If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your [Amplify configuration](/[platform]/build-a-backend/auth/existing-resources/) to `CUSTOM_AUTH`. +If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your Amplify configuration to `CUSTOM_AUTH`. For more information on authentication flow types, you can check out the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html). ## Register a user @@ -683,9 +619,9 @@ Confirm sign in succeeded ### Lambda Trigger Setup -AWS Amplify now supports creating functions as part of the AWS Amplify. For more information on the Functions and how to start with them check out [Functions documentation](/[platform]/build-a-backend/functions/). In addition, more information on available triggers can be found in the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). +AWS Amplify now supports creating functions as part of the AWS Amplify. For more information on the Functions and how to start with them check out [Functions documentation](/[platform]/build-a-backend/functions/). In addition, more information on available triggers can be found in the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html). -### Custom Auth Flow with SRP +### Custom Auth Flow with Secure Remote Password (SRP) Cognito User Pool allows to start the custom authentication flow with SRP as the first step. If you would like to use this flow, setup Define Auth Lambda trigger to handle SRP_A as the first challenge as shown below: @@ -793,64 +729,6 @@ RxAmplify.Auth.signIn("username", "password", options) -### CAPTCHA-based authentication - -Here is the sample for creating a CAPTCHA challenge with a Lambda Trigger. - -The `Create Auth Challenge Lambda Trigger` creates a CAPTCHA as a challenge to the user. The URL for the CAPTCHA image and the expected answer are added to the private challenge parameters: - -```javascript -export const handler = async (event) => { - if (!event.request.session || event.request.session.length === 0) { - event.response.publicChallengeParameters = { - captchaUrl: , - }; - event.response.privateChallengeParameters = { - answer: , - }; - event.response.challengeMetadata = "CAPTCHA_CHALLENGE"; - } - return event; -}; -``` - -This `Define Auth Challenge Lambda Trigger` defines a custom challenge: - -```javascript -export const handler = async (event) => { - if (!event.request.session || event.request.session.length === 0) { - // If we don't have a session or it is empty then send a CUSTOM_CHALLENGE - event.response.challengeName = "CUSTOM_CHALLENGE"; - event.response.failAuthentication = false; - event.response.issueTokens = false; - } else if (event.request.session.length === 1 && event.request.session[0].challengeResult === true) { - // If we passed the CUSTOM_CHALLENGE then issue token - event.response.failAuthentication = false; - event.response.issueTokens = true; - } else { - // Something is wrong. Fail authentication - event.response.failAuthentication = true; - event.response.issueTokens = false; - } - - return event; -}; -``` - -The `Verify Auth Challenge Response Lambda Trigger` is used to verify a challenge answer: - -```javascript -export const handler = async (event, context) => { - if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) { - event.response.answerCorrect = true; - } else { - event.response.answerCorrect = false; - } - - return event; -}; -``` -
@@ -859,13 +737,13 @@ The Auth category can be configured to perform a [custom authentication flow](ht ## Prerequisites A Flutter application targeting Flutter SDK >= 3.3.0 with Amplify libraries integrated. -Amplify requires a minimum target platform for iOS (13.0), Android (API level 24), and macOS (10.15). Additional setup is required for some target platforms. Please see the [platform setup](/[platform]/start/project-setup/platform-setup/) guide for more details on platform specific setup. +Amplify requires a minimum target platform for iOS (13.0), Android (API level 24), and macOS (10.15). Additional setup is required for some target platforms. Please see the [platform setup](/[platform]/build-a-backend/auth/connect-your-frontend/sign-in/#platform-setup) for more details on platform specific setup. ## Configure Auth The custom auth flow can be [configured manually](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html). -If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your [Amplify configuration](/[platform]/build-a-backend/auth/existing-resources/) to `CUSTOM_AUTH`. +If you have already configured custom auth, you can use the custom auth flow by changing the `authenticationFlowType` value in your Amplify configuration to `CUSTOM_AUTH`. For more information on authentication flow types, you can check out the [Cognito documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html). ## Register a user @@ -946,7 +824,7 @@ user has already signed in and a valid session is active. You must first call
## Confirm sign in with custom challenge -Get the custom challenge (`1234` in this case) from the user and pass it to the `confirmSignin()` api. +To get a custom challenge from the user, create an appropriate UI for the user to submit the required value, and pass that value into the `confirmSignin()` API. ```dart Future confirmSignIn(String generatedNumber) async { @@ -977,7 +855,7 @@ Exception: `NotAuthorizedException` with a message `Invalid session for the user The example in this documentation demonstrates the passwordless custom authentication flow. However, it is also possible to require that users supply a valid password as part of the custom authentication flow. -To require a valid password, you can alter the `DefineAuthChallenge` code to handle a `PASSWORD_VERIFIER` step: +To require a valid password, you can alter the [DefineAuthChallenge](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html) code to handle a `PASSWORD_VERIFIER` step: ```js exports.handler = async (event) => { 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 b6ef6df4250..1b00914f9a9 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 @@ -80,7 +80,7 @@ Be sure to add a "raw" folder under `app/src/main/res` directory if it does not ```bash title="Terminal" showLineNumbers={false} -npx ampx sandbox --outputs-out-dir app/src/main/res/raw +npx ampx sandbox --outputs-out-dir ``` @@ -92,7 +92,7 @@ npx ampx sandbox -After a successful deployment, this command also generates a configuration file (`amplify_outputs.json`) to enable your frontend app to connect to your backend resources. The values you configure in your backend authentication resource are set in the generated configuration file to automatically configure the frontend [`Authenticator connected component`](https://ui.docs.amplify.aws/react/connected-components/authenticator). +After a successful deployment, this command also generates an outputs file (`amplify_outputs.json`) to enable your frontend app to connect to your backend resources. The values you configure in your backend authentication resource are set in the generated outputs file to automatically configure the frontend [`Authenticator connected component`](https://ui.docs.amplify.aws/react/connected-components/authenticator). ## Connect your application code to your auth resource @@ -306,7 +306,7 @@ npx pod-install ``` -For calling native libraries and platform dependencies from Expo, you need to run the prebuild command for generating the folders for related platforms. +For calling native libraries and platform dependencies from Expo, you need to run the prebuild command for generating the folders for related platforms. ```bash title="Terminal" showLineNumbers={false} npx expo prebuild @@ -365,7 +365,7 @@ flutter pub add amplify_auth_cognito flutter pub add amplify_authenticator ``` -or you can update your `pubspec.yaml` file with the following +or you can update your `pubspec.yaml` file with the following ```yaml environment: @@ -482,7 +482,7 @@ Add the following dependencies to your *app*'s `build.gradle` file and click "Sy ```kotlin dependencies { - implementation("com.amplifyframework.ui:authenticator:1.1.0") + implementation("com.amplifyframework.ui:authenticator:ANDROID_AUTHENTICATOR_VERSION") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") } ``` @@ -492,10 +492,10 @@ dependencies { ```groovy dependencies { - dependencies { + dependencies { // Authenticator dependency - implementation 'com.amplifyframework.ui:authenticator:1.1.0' - + implementation 'com.amplifyframework.ui:authenticator:ANDROID_AUTHENTICATOR_VERSION' + // Support for Java 8 features coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' } @@ -505,12 +505,23 @@ dependencies { + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + ```kotlin title="MyAmplifyApp.kt" import android.app.Application import android.util.Log import com.amplifyframework.AmplifyException import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.core.Amplify +import com.amplifyframework.core.configuration.AmplifyOutputs class MyAmplifyApp: Application() { override fun onCreate() { @@ -519,7 +530,7 @@ class MyAmplifyApp: Application() { try { // highlight-next-line Amplify.addPlugin(AWSCognitoAuthPlugin()) - Amplify.configure(applicationContext) + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error) @@ -594,7 +605,7 @@ An application with Amplify libraries integrated and a minimum target of any of -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. +visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). @@ -646,7 +657,7 @@ struct MyApp: App { init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) } catch { print("Unable to configure Amplify \(error)") } diff --git a/src/pages/[platform]/build-a-backend/auth/sign-in-with-web-ui/index.mdx b/src/pages/[platform]/build-a-backend/auth/sign-in-with-web-ui/index.mdx new file mode 100644 index 00000000000..13ae1f661ff --- /dev/null +++ b/src/pages/[platform]/build-a-backend/auth/sign-in-with-web-ui/index.mdx @@ -0,0 +1,401 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Enable sign-in with web UI', + description: + 'Use Amazon Cognito Auth plugin to register and authenticate a user with a prebuilt web UI', + platforms: ['flutter', 'swift', 'android'] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + + +## Prerequisites +* An app set up according to the [getting started walkthrough](/[platform]/build-a-backend/auth/set-up-auth/) + + + When configuring social sign-in, it's important to exercise caution when designating attributes as "required." Different social identity providers have varied scopes in terms of the information they respond back to Cognito with. User pool attributes that are initially set up as "required" cannot be changed later, and may require you to migrate the users or create a new user pool. + + +## Configure Auth Category + + + +This library's Cognito plugin currently supports the [Authorization Code Grant](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html) OAuth Flow. + + + +In your `auth/resource.ts` file, update the +```ts +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + }, + }, +}); +``` + +## 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: + +```xml + + ... + + + + + + + + + ... + +``` + +## Launch Web UI Sign In + +Sweet! You're now ready to launch sign in with web UI. For now, just add this method to the `onCreate` method of MainActivity: + + + + +```java +Amplify.Auth.signInWithWebUI( + this, + result -> Log.i("AuthQuickStart", result.toString()), + error -> Log.e("AuthQuickStart", error.toString()) +); +``` + + + + +```kotlin +Amplify.Auth.signInWithWebUI( + this, + { Log.i("AuthQuickStart", "Signin OK = $it") }, + { Log.e("AuthQuickStart", "Signin failed", it) } +) +``` + + + + +```kotlin +try { + val result = Amplify.Auth.signInWithWebUI(this) + Log.i("AuthQuickStart", "Signin OK: $result") +} catch (error: AuthException) { + Log.e("AuthQuickStart", "Signin failed", error) +} +``` + + + + +```java +RxAmplify.Auth.signInWithWebUI(this) + .subscribe( + result -> Log.i("AuthQuickStart", result.toString()), + error -> Log.e("AuthQuickStart", error.toString()) + ); +``` + + + + + + + +## Prerequisites + + + +**Note:** Social sign-in (OAuth) functionality is only available in **iOS** and **macOS**. + + When configuring social sign-in, it's important to exercise caution when designating attributes as "required." Different social identity providers have varied scopes in terms of the information they respond back to Cognito with. User pool attributes that are initially set up as "required" cannot be changed later, and may require you to migrate the users or create a new user pool. + + + +An application with a minimum target of **iOS 13.0** and/or **macOS 10.15** with Amplify libraries integrated, using **Xcode 14.1** or later. + +For a full example, please follow the [project setup walkthrough](/[platform]/start/project-setup/prerequisites/). + + + +To use Auth in a macOS project, you'll need to enable the Keychain Sharing capability. In Xcode, navigate to **your application target** > **Signing & Capabilities** > **+ Capability**, then select **Keychain Sharing.** + +This capability is required because Auth uses the Data Protection Keychain on macOS as a platform best practice. +See [TN3137: macOS keychain APIs and implementations](https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains) for more information on how Keychain works on macOS and the Keychain Sharing entitlement. + +For more information on adding capabilities to your application, see [Xcode Capabilities](https://developer.apple.com/documentation/xcode/capabilities). + + + +## Configure Auth Category + + + +This library's Cognito plugin currently supports the [Authorization Code Grant](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html) OAuth Flow. + + + +Update the `auth/resource.ts` file like the following to enable the sign-in and sign-out capabilities with a web ui. + +```ts +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + }, + }, +}); +``` +## Update Info.plist + +Signin with web UI require the Amplify plugin to show up the signin UI inside a webview. After the signin 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 + + + + + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + + + + +``` + +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 Web UI Sign In + +You're now ready to launch sign in with web UI. The `signInWithWebUI` api require a presentationAnchor and for an iOS app it will be the main UIWindow of the app. The example code below assume that you are in a UIViewController where you can fetch the UIWindow instance by `self.view.window`. + + + + + +```swift +func signInWithWebUI() async { + do { + let signInResult = try await Amplify.Auth.signInWithWebUI(presentationAnchor: self.view.window!) + if signInResult.isSignedIn { + print("Sign in succeeded") + } + } catch let error as AuthError { + print("Sign in failed \(error)") + } catch { + print("Unexpected error: \(error)") + } +} +``` + + + + + +```swift +func signInWithWebUI() -> AnyCancellable { + Amplify.Publisher.create { + try await Amplify.Auth.signInWithWebUI(presentationAnchor: self.view.window!) + }.sink { + if case let .failure(authError) = $0 { + print("Sign in failed \(authError)") + } + } + receiveValue: { signInResult in + if signInResult.isSignedIn { + print("Sign in succeeded") + } + } +} +``` + + + + + +### Prefer private session during signIn + +Starting Amplify 1.6.0, `Amplify.Auth.signInWithWebUI` automatically uses [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) internally for iOS 13.0+. For older iOS versions, it will fall back to [SFAuthenticationSession](https://developer.apple.com/documentation/safariservices/sfauthenticationsession). +This release also introduces a new `preferPrivateSession` flag to `AWSAuthWebUISignInOptions` during the sign in flow. If `preferPrivateSession` is set to `true` during sign in, the user will not see a web view displayed when they sign out. `preferPrivateSession` will set [ASWebAuthenticationSession.prefersEphemeralWebBrowserSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237231-prefersephemeralwebbrowsersessio) internally and the authentication session will be private if the user's preferred browser supports it. + +```swift +try await Amplify.Auth.signInWithWebUI( + presentationAnchor: self.view.window!, + options: .preferPrivateSession() +) { + ... +} +``` + + + +## Prerequisites + +* An app set up according to the [getting started walkthrough](/[platform]/build-a-backend/auth/set-up-auth/) + + + When configuring Social sign-in, it's important to exercise caution when designating attributes as "required." Different social identity providers have varied scopes in terms of the information they respond back to Cognito with. User pool attributes that are initially set up as "required" cannot be changed later, and may require you to migrate the users or create a new user pool. + + +## Configure Auth Category + + + +This library's Cognito plugin currently supports the [Authorization Code Grant](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html) OAuth Flow. + + + +Update the `auth/resource.ts` file like the following to enable the sign-in and sign-out capabilities with a web ui. + +```ts +export const auth = defineAuth({ + loginWith: { + email: true, + externalProviders: { + callbackUrls: ["myapp://callback/"], + logoutUrls: ["myapp://signout/"], + }, + }, +}); +``` + +## 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. + +## Platform Setup + +### Web + +To use Hosted UI in your Flutter web application locally, you must run the app with the `--web-port=3000` argument (with the value being whichever port you assigned to localhost host when configuring your redirect URIs). + +### Android + +Add the following `queries` element to the `AndroidManifest.xml` file in your app's `android/app/src/main` directory, as well as the following `intent-filter` to the `MainActivity` in the same file. + +Replace `myapp` with your redirect URI scheme as necessary: + +```xml + + + + + + + ... + + + + + + + + + ... + +``` + +### macOS + +Open XCode and enable the App Sandbox capability and then select "Incoming Connections (Server)" under "Network". + +![Incoming Connections setting selected in the App Sandbox section of the runner signing and capabilities tab.](/images/project-setup/flutter/mac/xcode-entitlements.png) + +### iOS, Windows and Linux + +No specific platform configuration is required. + +## Launch Web UI Sign In + +You're now ready to launch sign in with web UI. + +```dart +Future signInWithWebUI() async { + try { + final result = await Amplify.Auth.signInWithWebUI(); + safePrint('Sign in result: $result'); + } on AuthException catch (e) { + safePrint('Error signing in: ${e.message}'); + } +} +``` + +You can also specify a provider with the `provider` attribute: + +```dart +Future signInWithWebUIProvider() async { + try { + final result = await Amplify.Auth.signInWithWebUI( + provider: AuthProvider.google, + ); + safePrint('Result: $result'); + } on AuthException catch (e) { + safePrint('Error signing in: ${e.message}'); + } +} +``` + +Amplify Flutter currently supports the following social sign-in providers: + * Google + * Facebook + * Login With Amazon + * Apple + +### Prefer private session during signIn on iOS. + +Amplify.Auth.signInWithWebUI uses [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) internally for iOS. ASWebAuthenticationSession has a property, [prefersEphemeralWebBrowserSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237231-prefersephemeralwebbrowsersessio) which can be used to indicate whether the session should ask the browser for a private authentication session. To set this flag to true, set `preferPrivateSession` to true using `CognitoSignInWithWebUIPluginOptions`. + +This will bypass the permissions dialog that is displayed to the end user during sign in and sign out. However, it will also prevent reuse of existing sessions from the user's browser. For example, if the user is logged into Google in their browser and try to sign in using Google in your app, they would now need to re-enter their credentials. + +```dart +Future signInWithWebUIAndPrivateSession() async { + await Amplify.Auth.signInWithWebUI( + options: const SignInWithWebUIOptions( + pluginOptions: CognitoSignInWithWebUIPluginOptions( + isPreferPrivateSession: true, + ), + ), + ); +} +``` + + 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 347a0ea052d..c4c14b00886 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 @@ -417,7 +417,7 @@ final lambdaResponse = await Amplify.API.query(request: lambdaRequest).response; ## Set custom request headers -When working with the Amplify Data endpoint, you may need to set request headers for authorization purposes or to pass additional metadata from your frontend to the backend API. +When working with the Amplify Data endpoint, you may need to set request headers for authorization purposes or to pass additional metadata from your frontend to the backend API. @@ -618,7 +618,7 @@ struct CustomInterceptor: URLRequestInterceptor { } let apiPlugin = try AWSAPIPlugin() try Amplify.addPlugin(apiPlugin) -try Amplify.configure() +try Amplify.configure(with: .amplifyOutputs) try apiPlugin.add(interceptor: CustomInterceptor(), for: AWSAPIPlugin.defaultGraphQLAPI) ``` 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 6e52ac00f17..51fa0797d19 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 @@ -86,12 +86,6 @@ Function access can only be configured on the schema object. It cannot be config ## Access the API using `aws-amplify` - - -**Under active development:** Configuring the `aws-amplify` data client is under active development. The current experience has rough edges and may change between versions of `@aws-amplify/backend`. Try it out and provide feedback at https://github.com/aws-amplify/amplify-backend/issues/new/choose - - - In the handler file for your function, configure the Amplify data client ```ts title="amplify/functions/data-access.ts" diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx index b6b8243dddc..811bcd7a8d6 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx @@ -54,14 +54,14 @@ In the example above, everyone (`public`) can read every Post but authenticated Use the guide below to select the correct authorization strategy for your use case: -| **Recommended use case** | **Strategy** | **Provider** | +| **Recommended use case** | **Strategy** | **`authMode`** | |---|---|---| -| [Public data access where users or devices are anonymous. Anyone with the AppSync API key is granted access.](/[platform]/build-a-backend/data/customize-authz/public-data-access) | `public` | `apiKey` | -| [Recommended for production environment's public data access. Public data access where unauthenticated users or devices are granted permissions using Amazon Cognito identity pool's role for unauthenticated identities.]( /[platform]/build-a-backend/data/customize-authz/public-data-access/#add-public-authorization-rule-using-iam-authentication) | `public` | `identityPool` | -| [Per user data access. Access is restricted to the "owner" of a record. Leverages `amplify/auth/resource.ts` Cognito user pool by default.](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access) | `owner` | `userPools` / `oidc` | -| [Any signed-in data access. Unlike owner-based access, **any** signed-in user has access.](/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access) | `private` | `userPools` / `oidc` / `identityPool` | -| [Per user group data access. A specific or dynamically configured group of users has access.](/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access) | `group` | `userPools` / `oidc` | -| [Define your own custom authorization rule within a serverless function.](/[platform]/build-a-backend/data/customize-authz/custom-data-access-patterns) | `custom` | `function` | +| [Public data access where users or devices are anonymous. Anyone with the AppSync API key is granted access.](/[platform]/build-a-backend/data/customize-authz/public-data-access) | `publicApiKey` | `apiKey` | +| [Recommended for production environment's public data access. Public data access where unauthenticated users or devices are granted permissions using Amazon Cognito identity pool's role for unauthenticated identities.]( /[platform]/build-a-backend/data/customize-authz/public-data-access/#add-public-authorization-rule-using-iam-authentication) | `guest` | `identityPool` | +| [Per user data access. Access is restricted to the "owner" of a record. Leverages `amplify/auth/resource.ts` Cognito user pool by default.](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access) | `owner`/`ownerDefinedIn`/`ownersDefinedIn` | `userPools` / `oidc` | +| [Any signed-in data access. Unlike owner-based access, **any** signed-in user has access.](/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access) | `authenticated` | `userPools` / `oidc` / `identityPool` | +| [Per user group data access. A specific or dynamically configured group of users has access.](/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access) | `group`/`groupDefinedIn`/`groups`/`groupsDefinedIn` | `userPools` / `oidc` | +| [Define your own custom authorization rule within a serverless function.](/[platform]/build-a-backend/data/customize-authz/custom-data-access-patterns) | `custom` | `lambda` | ## Understand how authorization rules are applied diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/multi-user-data-access/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/multi-user-data-access/index.mdx index 172e3bd589f..df8a81958d3 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/multi-user-data-access/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/multi-user-data-access/index.mdx @@ -33,7 +33,7 @@ The `ownersDefinedIn` rule grants a set of users access to a record by automatic ## Add multi-user authorization rule -If you want to grant a set of users access to a record, you use the `ownersDefinedIn` rule. This automatically creates a `owner: a.string().array()` field to store the allowed owners. +If you want to grant a set of users access to a record, you use the `ownersDefinedIn` rule. This automatically creates a `owners: a.string().array()` field to store the allowed owners. ```ts title="amplify/data/resource.ts" const schema = a.schema({ @@ -70,7 +70,7 @@ const { errors, data: newTodo } = await client.models.Todo.create( await client.models.Todo.update( { id: newTodo.id, - owner: [...(newTodo.owner as string[]), otherUserId], + owners: [...(newTodo.owners as string[]), otherUserId], }, // highlight-start { 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 f6f0d762c01..3e69704e06a 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 @@ -78,7 +78,7 @@ const schema = a.schema({ }); ``` -In your application, you can perform CRUD operations against the model using `client.models.` with the `iam` auth mode. +In your application, you can perform CRUD operations against the model using `client.models.` with the `identityPool` auth mode. If you're not using the auto-generated **amplify_outputs.json** file, then you must set the Amplify Library resource configuration's `allowGuestAccess` flag to `true`. This lets the Amplify Library use the unauthenticated role from your Cognito identity pool when your user isn't logged in. @@ -90,7 +90,7 @@ import outputs from "../amplify_outputs.json"; Amplify.configure( { - ...config, + ...outputs, Auth: { Cognito: { identityPoolId: config.aws_cognito_identity_pool_id, diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access/index.mdx index b0b914a8a30..0b647a5f8b6 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access/index.mdx @@ -28,14 +28,14 @@ export function getStaticProps(context) { }; } -The `private` authorization strategy restricts record access to only signed-in users authenticated through IAM, Cognito, or OpenID Connect, applying the authorization rule to all users. It provides a simple way to make data private to all authenticated users. +The `authenticated` authorization strategy restricts record access to only signed-in users authenticated through IAM, Cognito, or OpenID Connect, applying the authorization rule to all users. It provides a simple way to make data private to all authenticated users. ## Add signed-in user authorization rule -You can use the `private` authorization strategy to restrict a record's access to every signed-in user. +You can use the `authenticated` authorization strategy to restrict a record's access to every signed-in user. -**Note:** If you want to restrict a record's access to a specific user, see [Per-user/per-owner data access](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access/). The `private` authorization strategy detailed on this page applies the authorization rule for data access to **every** signed-in user. +**Note:** If you want to restrict a record's access to a specific user, see [Per-user/per-owner data access](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access/). The `authenticated` authorization strategy detailed on this page applies the authorization rule for data access to **every** signed-in user. In the example below, anyone with a valid JWT token from the Cognito user pool is allowed access to all Todos. @@ -109,4 +109,4 @@ const { errors, data: newTodo } = await client.models.Todo.create( ); ``` -In addition, you can also use OpenID Connect with `private` authorization. See [OpenID Connect as an authorization provider](/[platform]/build-a-backend/data/customize-authz/using-oidc-authorization-provider/). +In addition, you can also use OpenID Connect with `authenticated` authorization. See [OpenID Connect as an authorization provider](/[platform]/build-a-backend/data/customize-authz/using-oidc-authorization-provider/). 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 16a27e81a39..7946cea3726 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 @@ -2,7 +2,7 @@ import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; export const meta = { title: 'Optimistic UI', - description: 'Learn more about implementing optimistic UI with Amplify GraphQL API.', + description: 'Learn more about implementing optimistic UI with Amplify Data API.', platforms: [ 'javascript', 'swift', @@ -36,7 +36,7 @@ In the following examples we'll create a list view that optimistically renders n For more details on TanStack Query, including requirements, supported browsers, and advanced usage, see the [TanStack Query documentation](https://tanstack.com/query/latest/docs/react/overview). For complete guidance on how to implement optimistic updates with TanStack Query, see the [TanStack Query Optimistic UI Documentation](https://tanstack.com/query/latest/docs/react/guides/optimistic-updates). -For more on the Amplify GraphQL API, see the [API documentation](/gen1/[platform]/build-a-backend/graphqlapi/set-up-graphql-api/). +For more on Amplify Data, see the [API documentation](/[platform]/build-a-backend/data/set-up-data/). @@ -44,7 +44,7 @@ To get started, run the following command in an existing Amplify project with a ```bash title="Terminal" showLineNumbers={false} # Install TanStack Query -npm i @tanstack/react-query +npm i @tanstack/react-query @tanstack/react-query-devtools ``` Modify your Data schema to use this "Real Estate Property" example: @@ -112,7 +112,7 @@ For the complete working example, including required imports and React component -## How to use TanStack Query query keys with the GraphQL API +## How to use TanStack Query query keys with the Amplify Data API TanStack Query manages query caching based on the query keys you specify. A query key must be an array. The array can contain a single string or multiple strings and nested objects. The query key must be serializable, and unique to the query's data. @@ -122,7 +122,7 @@ For more detailed information on query keys, see the [TanStack Query documentati ## Optimistically rendering a list of records -To optimistically render a list of items returned from the GraphQL API, use the TanStack `useQuery` hook, passing in the GraphQL API query as the `queryFn` parameter. The following example creates a query to retrieve all records from the API. We'll use `realEstateProperties` as the query key, which will be the same key we use to optimistically render a newly created item. +To optimistically render a list of items returned from the Amplify Data API, use the TanStack `useQuery` hook, passing in the Data API query as the `queryFn` parameter. The following example creates a query to retrieve all records from the API. We'll use `realEstateProperties` as the query key, which will be the same key we use to optimistically render a newly created item. ```ts title="src/App.tsx" // highlight-start @@ -159,14 +159,14 @@ function App() { ## Optimistically rendering a newly created record -To optimistically render a newly created record returned from the GraphQL API, use the TanStack `useMutation` hook, passing in the GraphQL API mutation as the `mutationFn` parameter. We'll use the same query key used by the `useQuery` hook (`realEstateProperties`) as the query key to optimistically render a newly created item. +To optimistically render a newly created record returned from the Amplify Data API, use the TanStack `useMutation` hook, passing in the Amplify Data API mutation as the `mutationFn` parameter. We'll use the same query key used by the `useQuery` hook (`realEstateProperties`) as the query key to optimistically render a newly created item. We'll use the `onMutate` function to update the cache directly, as well as the `onError` function to rollback changes when a request fails. ```ts import { generateClient } from 'aws-amplify/api' import type { Schema } from '../amplify/data/resource' // highlight-next-line -import { useMutation } from '@tanstack/react-query' +import { useQueryClient, useMutation } from '@tanstack/react-query' const client = generateClient() @@ -247,7 +247,9 @@ function App() { queryFn: async () => { if (!currentRealEstatePropertyId) { return } - const { data: property } = await client.models.RealEstateProperty.get({ currentRealEstatePropertyId }); + const { data: property } = await client.models.RealEstateProperty.get({ + id: currentRealEstatePropertyId, + }); return property; }, }); @@ -269,6 +271,7 @@ When directly interacting with the cache via the `onMutate` function, the `newRe ```ts title="src/App.tsx" import { generateClient } from 'aws-amplify/data' import type { Schema } from '../amplify/data/resource' +import { useQueryClient, useMutation } from "@tanstack/react-query"; const client = generateClient() @@ -351,7 +354,7 @@ We'll use the `onMutate` function to update the cache directly, as well as the ` ```ts title="src/App.tsx" import { generateClient } from 'aws-amplify/data' import type { Schema } from '../amplify/data/resource' -import { useMutation } from '@tanstack/react-query' +import { useQueryClient, useMutation } from '@tanstack/react-query' const client = generateClient() @@ -1007,7 +1010,7 @@ actor RealEstatePropertyList { } ``` -Calling the `listProperties()` method will perform a query with GraphQL API and store the results in the `properties` property. When this property is set, the list is sent back to the subscribers. In your UI, create a view model and subscribe to updates: +Calling the `listProperties()` method will perform a query with Amplify Data API and store the results in the `properties` property. When this property is set, the list is sent back to the subscribers. In your UI, create a view model and subscribe to updates: ```swift class RealEstatePropertyContainerViewModel: ObservableObject { @@ -1045,7 +1048,7 @@ struct RealEstatePropertyContainerView: View { ## Optimistically rendering a newly created record -To optimistically render a newly created record returned from the GraphQL API, add a method to the `actor RealEstatePropertyList`: +To optimistically render a newly created record returned from the Amplify Data API, add a method to the `actor RealEstatePropertyList`: ```swift func createProperty(name: String, address: String? = nil) { @@ -1108,7 +1111,7 @@ func updateProperty(_ property: RealEstateProperty) async { ## Optimistically render deleting a record -To optimistically render a GraphQL API delete, use the code snippet like below: +To optimistically render a Amplify Data API delete, use the code snippet like below: ```swift func deleteProperty(_ property: RealEstateProperty) async { @@ -1159,7 +1162,7 @@ struct OptimisticUIApp: App { do { Amplify.Logging.logLevel = .verbose try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with API, Storage, and Auth plugins!") } catch { print("Failed to initialize Amplify with \(error)") 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 cb600c7d978..6e6ae3352a5 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 @@ -89,7 +89,7 @@ npx ampx sandbox ```bash title="Terminal" showLineNumbers={false} -npx ampx sandbox --outputs-out-dir app/src/main/res/raw/ +npx ampx sandbox --outputs-out-dir ``` @@ -117,7 +117,7 @@ Once the cloud sandbox is up and running, it will also create an `amplify_output To connect your frontend code to your backend, you need to: -1. configure the Amplify library with the Amplify client configuration file +1. configure the Amplify library with the Amplify client configuration file (`amplify_outputs.json`) 2. generate a new API client from the Amplify library 3. make an API request with end-to-end type-safety @@ -148,8 +148,7 @@ Under Gradle Scripts, open build.gradle (Module :app), add the following lines: dependencies { // Amplify API dependencies // highlight-start - implementation("com.amplifyframework:aws-api:2.14.11") - implementation("com.amplifyframework:core:2.14.11") + implementation("com.amplifyframework:aws-api:ANDROID_VERSION") // highlight-end // ... other dependencies } @@ -161,6 +160,16 @@ Next, configure the Amplify client library with the generated `amplify_outputs.j Create a new `MyAmplifyApp` class that inherits from `Application` with the following code: + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + ```kt package com.example.myapplication @@ -169,6 +178,7 @@ import android.util.Log import com.amplifyframework.AmplifyException import com.amplifyframework.api.aws.AWSApiPlugin import com.amplifyframework.core.Amplify +import com.amplifyframework.core.configuration.AmplifyOutputs class MyAmplifyApp : Application() { override fun onCreate() { @@ -180,8 +190,7 @@ class MyAmplifyApp : Application() { Amplify.addPlugin(AWSApiPlugin()) // Configures the client library to be aware of your backend API // endpoint and authorization modes. - Amplify.configure(applicationContext) - + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) Log.i("Tutorial", "Initialized Amplify") } catch (error: AmplifyException) { Log.e("Tutorial", "Could not initialize Amplify", error) @@ -251,7 +260,7 @@ struct MyAmplifyApp: App { let awsApiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels()) do { try Amplify.add(plugin: awsApiPlugin) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Initialized Amplify"); } catch { // simplified error handling for the tutorial diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx new file mode 100644 index 00000000000..903b0612fcb --- /dev/null +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -0,0 +1,1521 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Working with files/attachments', + description: + 'Working with files/attachments.', + 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 + } + }; +} + +The Storage and GraphQL API categories can be used together to associate a file, such as an image or video, with a particular record. For example, you might create a `User` model with a profile picture, or a `Post` model with an associated image. With Amplify's GraphQL API and Storage categories, you can reference the file within the model itself to create an association. + +## Set up the project + +Set up your project by following the instructions in the [Quickstart guide](/[platform]/start/quickstart/). + +## Define the model + +Open `amplify/data/resource.ts` and add the following model as shown below: + +```ts title="amplify/data/resource.ts" +import { type ClientSchema, a, defineData } from "@aws-amplify/backend"; + +const schema = a.schema({ + Song: a + .model({ + id: a.id().required(), + name: a.string().required(), + coverArtPath: a.string(), + }) + .authorization((allow) => [allow.publicApiKey()]), +}); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: "apiKey", + + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); + +``` +## Setup the Storage + +Next, Let's configure Storage and allow access to all authenticated(signed in) users of your application. create a file `amplify/storage/resource.ts` and add the following code,This will restrict file access to only the signed-in user. + +```ts title="amplify/storage/resource.ts" + +import { defineStorage } from "@aws-amplify/backend"; + +export const storage = defineStorage({ + name: "amplify-gen2-files", + access: (allow) => ({ + "images/*": [allow.authenticated.to(["read", "write", "delete"])], + }), +}); + + +``` + +Configure the storage in the `amplify/backend.ts` file as demonstrated below: + +```ts title="amplify/backend.ts" + +import { defineBackend } from "@aws-amplify/backend"; +import { auth } from "./auth/resource"; +import { data } from "./data/resource"; +import { storage } from "./storage/resource"; + +export const backend = defineBackend({ + auth, + data, + storage, +}); + +``` + + +## Configuring authorization + +Your application needs authorization credentials for reading and writing to both Storage and the Data, except in the case where all data and files are intended to be publicly accessible. + +The Storage and Data categories govern data access based on their own authorization patterns, meaning that it's necessary to configure appropriate auth roles for each individual category. Although both categories share the same access credentials set up through the Auth category, they work independently from one another. For instance, adding an `allow.authenticated()` to the Data does not guard against file access in the Storage category. Likewise, adding authorization rules to the Storage category does not guard against data access in the API. + +When you configure Storage, Amplify will configure appropriate IAM policies on the bucket using a Cognito Identity Pool role. You will then have the option of adding CRUD (Create, Update, Read and Delete) based permissions as well, so that Authenticated and Guest users will be granted limited permissions within these levels. **Even after adding this configuration, all Storage access is still `guest` by default.** To guard against accidental public access, the Storage access levels must either be configured on the Storage object globally, or set within individual function calls. This guide uses the former approach, setting all Storage access to `authenticated` users. + +The ability to independently configure authorization rules for each category allows for more granular control over data access, and adds greater flexibility. For scenarios where authorization patterns must be mixed and matched, configure the access level on individual Storage function calls. For example, you may want to use `entity_id` CRUD access on an individual Storage function call for files that should only be accessible by the owner (such as personal files), `authenticated` read access to allow all logged in users to view common files (such as images in a shared photo album), and `guest` read access to allow all users to view a file (such as a public profile picture). + +For more details on how to configure Storage authorization levels, see the [Storage documentation](/[platform]/build-a-backend/storage/authorization/). For more on configuring Data authorization, see the [API documentation](/[platform]/build-a-backend/data/customize-authz/). + +## Create a record with an associated file + +You can create a record via the Amplify Data client, upload a file to Storage, and finally update the record to associate it with the uploaded file. Use the following example with the Amplify Data client and Amplify Storage library helpers, `uploadData` and `getUrl`, to create a record and associate it the file with the record. + + + +The API record's `id` is prepended to the Storage file name to ensure uniqueness. If this is excluded, multiple API records could then be associated with the same file key unintentionally. + + + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Create the API record: +const response = await client.models.Song.create({ + name: `My first song`, +}); + +const song = response.data; + +if (!song) return; + +// Upload the Storage file: +const result = await uploadData({ + path: `images/${song.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, +}).result; + +// Add the file association to the record: +const updateResponse = await client.models.Song.update({ + id: song.id, + coverArtPath: result?.path, +}); + +const updatedSong = updateResponse.data; + +setCurrentSong(updatedSong); + +// If the record has no associated file, we can return early. +if (!updatedSong.coverArtPath) return; + +// Retrieve the file's signed URL: +const signedURL = await getUrl({ path: updatedSong.coverArtPath }); + + +``` + +## Add or update a file for an associated record + +To associate a file with a record, update the record with the key returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's key, then retrieves the signed URL to download the image. If an image is already associated with the record, this will update the record with the new image. + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Upload the Storage file: +const result = await uploadData({ + path: `images/${currentSong.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, +}).result; + +// Add the file association to the record: +const response = await client.models.Song.update({ + id: currentSong.id, + coverArtPath: result?.path, +}); + +const updatedSong = response.data; + +setCurrentSong(updatedSong); + +// If the record has no associated file, we can return early. +if (!updatedSong?.coverArtPath) return; + +// Retrieve the file's signed URL: +const signedURL = await getUrl({ path: updatedSong.coverArtPath }); + +``` + +## Query a record and retrieve the associated file + +To retrieve the file associated with a record, first query the record, then use Storage to get the signed URL. The signed URL can then be used to download the file, display an image, etc: + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +const response = await client.models.Song.get({ + id: currentSong.id, +}); +const song = response.data; + +// If the record has no associated file, we can return early. +if (!song?.coverArtPath) return; + +// Retrieve the signed URL: +const signedURL = await getUrl({ path: song.coverArtPath }); + +``` + +## Delete and remove files associated with API records + +There are three common deletion workflows when working with Storage files and the GraphQL API: + +1. Remove the file association, continue to persist both file and record. +2. Remove the record association and delete the file. +3. Delete both file and record. + +### Remove the file association, continue to persist both file and record + +The following example removes the file association from the record, but does not delete the file from S3, nor the record from the database. + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +const response = await client.models.Song.get({ + id: currentSong.id, +}); + +const song = response.data; + +// If the record has no associated file, we can return early. +if (!song?.coverArtPath) return; + +const updatedSong = await client.models.Song.update({ + id: song.id, + coverArtPath: null, +}); + +``` +### Remove the record association and delete the file + +The following example removes the file from the record, then deletes the file from S3: + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { remove } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); +const response = await client.models.Song.get({ + id: currentSong.id, +}); +const song = response?.data; + +// If the record has no associated file, we can return early. +if (!song?.coverArtPath) return; + +// Remove associated file from record +const updatedSong = await client.models.Song.update({ + id: song.id, + coverArtPath: null, +}); + +// Delete the file from S3: +await remove({ path: song.coverArtPath }); + +``` + +### Delete both file and record + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { remove } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); +const response = await client.models.Song.get({ + id: currentSong.id, +}); + +const song = response.data; + +// If the record has no associated file, we can return early. +if (!song?.coverArtPath) return; + +await remove({ path: song.coverArtPath }); + +// Delete the record from the API: +await client.models.Song.delete({ id: song.id }); + +``` + +## Working with multiple files + +You may want to add multiple files to a single record, such as a user profile with multiple images. To do this, you can add a list of file keys to the record. The following example adds a list of file keys to a record: + +### GraphQL schema to associate a data model with multiple files + +Add the following model in `amplify/data/resource.ts" file. + +```ts title="amplify/data/resource.ts" +const schema = a.schema({ + PhotoAlbum: a + .model({ + id: a.id().required(), + name: a.string().required(), + imagePaths: a.string().array(), + }) + .authorization((allow) => [allow.publicApiKey()]), +}); +``` + +CRUD operations when working with multiple files is the same as when working with a single file, with the exception that we are now working with a list of image keys, as opposed to a single image key. + +### Create a record with multiple associated files + +First create a record via the GraphQL API, then upload the files to Storage, and finally add the associations between the record and files. + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Create the API record: +const response = await client.models.PhotoAlbum.create({ + name: `My first photoAlbum`, +}); + +const photoAlbum = response.data.createPhotoAlbum; + +if (!photoAlbum) return; + +// Upload all files to Storage: +const imagePaths = await Promise.all( + Array.from(e.target.files).map(async (file) => { + const result = await uploadData({ + path: `images/${photoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + return result.path; + }) +); + +const updatePhotoAlbumDetails = { + id: photoAlbum.id, + imagePaths: imagePaths, +}; + +// Add the file association to the record: +const updateResponse = await client.graphql({ + query: mutations.updatePhotoAlbum, + variables: { input: updatePhotoAlbumDetails }, +}); + +const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum; + +// If the record has no associated file, we can return early. +if (!updatedPhotoAlbum.imageKeys?.length) return; + +// Retrieve signed urls for all files: +const signedUrls = await Promise.all( + updatedPhotoAlbum?.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) +); + +``` + +### Add new files to an associated record + +To associate additional files with a record, update the record with the keys returned by the Storage uploads. + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Upload all files to Storage: +const newimagePaths = await Promise.all( + Array.from(e.target.files).map(async (file) => { + const result = await uploadData({ + path: `images/${currentPhotoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + return result.path; + }) +); + +// Query existing record to retrieve currently associated files: +const queriedResponse = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); + +const photoAlbum = queriedResponse.data; + +if (!photoAlbum?.imagePaths) return; + +// Merge existing and new file paths: +const updatedimagePaths = [...newimagePaths, ...photoAlbum.imagePaths]; + +// Update record with merged file associations: +const response = await client.models.PhotoAlbum.update({ + id: currentPhotoAlbum.id, + imagePaths: updatedimagePaths, +}); + +const updatedPhotoAlbum = response.data; + +// If the record has no associated file, we can return early. +if (!updatedPhotoAlbum?.imageKeys) return; + +// Retrieve signed urls for merged image paths: +const signedUrls = await Promise.all( + updatedPhotoAlbum?.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) +); + +``` + +### Update the file for an associated record + +Updating a file for an associated record is the same as updating a file for a single file record, with the exception that you will need to update the list of file keys. + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Upload new file to Storage: +const result = await uploadData({ + path: `images/${currentPhotoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, +}).result; + +const newFilePath = result.path; + +// Query existing record to retrieve currently associated files: +const queriedResponse = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); + +const photoAlbum = queriedResponse.data; + +if (!photoAlbum?.imagePaths?.length) return; + +// Retrieve last image path: +const [lastImagePath] = photoAlbum.imagePaths.slice(-1); + +// Remove last file association by key +const updatedimagePaths = [ + ...photoAlbum.imagePaths.filter((key) => key !== lastImagePath), + newFilePath, +]; + +// Update record with updated file associations: +const response = await client.models.PhotoAlbum.update({ + id: currentPhotoAlbum.id, + imagePaths: updatedimagePaths, +}); + +const updatedPhotoAlbum = response.data; + +// If the record has no associated file, we can return early. +if (!updatedPhotoAlbum?.imagePaths) return; + +// Retrieve signed urls for merged image paths: +const signedUrls = await Promise.all( + updatedPhotoAlbum?.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) +); + +``` + +### Query a record and retrieve the associated files + +To retrieve the files associated with a record, first query the record, then use Storage to retrieve all of the signed URLs. + +```ts title="src/App.tsx" +async function getImagesForPhotoAlbum() { +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +// Query the record to get the file paths: +const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); +const photoAlbum = response.data; + +// If the record has no associated files, we can return early. +if (!photoAlbum?.imagePaths) return; + +// Retrieve the signed URLs for the associated images: +const signedUrls = await Promise.all( + photoAlbum.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + return await getUrl({ path: imagePath }); + }) +); + +``` + +### Delete and remove files associated with API records + +The workflow for deleting and removing files associated with API records is the same as when working with a single file, except that when performing a delete you will need to iterate over the list of files keys and call `Storage.remove()` for each file. + +#### Remove the file association, continue to persist both files and record + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); + +const photoAlbum = response.data; + +// If the record has no associated file, we can return early. +if (!photoAlbum?.imagePaths) return; + +const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: null, +}); + +``` + +#### Remove the record association and delete the files + +```ts title="src/App.tsx" +import { generateClient } from "aws-amplify/api"; +import { remove } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); + +const photoAlbum = response.data; + +// If the record has no associated files, we can return early. +if (!photoAlbum?.imagePaths) return; + +// Remove associated files from record +const updateResponse = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: null, // Set the file association to `null` +}); + +const updatedPhotoAlbum = updateResponse.data; + +// Delete the files from S3: +await Promise.all( + photoAlbum?.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + await remove({ path: imagePath }); + }) +); + +``` + +#### Delete record and all associated files + +```ts title="src/App.tsx" + +import { generateClient } from "aws-amplify/api"; +import { remove } from "aws-amplify/storage"; +import type { Schema } from "../amplify/data/resource"; + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, +}); + +const photoAlbum = response.data; + +if (!photoAlbum) return; + +await client.models.PhotoAlbum.delete({ + id: photoAlbum.id, +}); + +setCurrentPhotoAlbum(null); + +// If the record has no associated file, we can return early. +if (!photoAlbum?.imagePaths) return; + +await Promise.all( + photoAlbum?.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + await remove({ path: imagePath }); + }) +); + +``` + +## Data consistency when working with records and files + +The recommended access patterns in these docs attempt to remove deleted files, but favor leaving orphans over leaving records that point to non-existent files. This optimizes for read latency by ensuring clients _rarely_ attempt to fetch a non-existent file from Storage. However, any app that deletes files can inherently cause records _on-device_ to point to non-existent files. + +One example is when we [create an API record, associate the Storage file with that record, and then retrieve the file's signed URL](#create-a-record-with-an-associated-file). "Device A" calls the GraphQL API to create `API_Record_1`, and then associates that record with `First_Photo`. Before "Device A" is about to retrieve the signed URL, "Device B" might query `API_Record_1`, delete `First_Photo`, and update the record accordingly. However, "Device A" is still using the old `API_Record_1`, which is now out-of-date. Even though the shared global state is correctly in sync at every stage, the individual device ("Device A") has an out-of-date record that points to a non-existent file. Similar issues can conceivably occur for updates. Depending on your app, some of these mismatches can be minimized _even more_ with [real-time data / GraphQL subscriptions](/[platform]/build-a-backend/graphqlapi/subscribe-data/). + +It is important to understand when these mismatches can occur and to add meaningful error handling around these cases. This guide does not include exhaustive error handling, real-time subscriptions, re-querying of outdated records, or attempts to retry failed operations. However, these are all important considerations for a production-level application. + +## Complete examples + + + + +```ts title="src/App.tsx" + +import "./App.css"; +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl, remove } from "aws-amplify/storage"; +import React, { useState } from "react"; +import type { Schema } from "../amplify/data/resource"; +import "@aws-amplify/ui-react/styles.css"; +import { + type WithAuthenticatorProps, + withAuthenticator, +} from "@aws-amplify/ui-react"; +import { Amplify } from "aws-amplify"; +import outputs from "../amplify_outputs.json"; + +Amplify.configure(outputs); + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +type Song = Schema["Song"]["type"]; + +function App({ signOut, user }: WithAuthenticatorProps) { + + const [currentSong, setCurrentSong] = useState(null); + + // Used to display image for current song: + const [currentImageUrl, setCurrentImageUrl] = useState< + string | null | undefined + >(""); + + async function createSongWithImage(e: React.ChangeEvent) { + if (!e.target.files) return; + const file = e.target.files[0]; + try { + + // Create the API record: + const response = await client.models.Song.create({ + name: `My first song`, + }); + + const song = response.data; + + if (!song) return; + + // Upload the Storage file: + const result = await uploadData({ + path: `images/${song.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + // Add the file association to the record: + const updateResponse = await client.models.Song.update({ + id: song.id, + coverArtPath: result?.path, + }); + + const updatedSong = updateResponse.data; + setCurrentSong(updatedSong); + + // If the record has no associated file, we can return early. + if (!updatedSong?.coverArtPath) return; + + // Retrieve the file's signed URL: + const signedURL = await getUrl({ path: updatedSong.coverArtPath }); + + setCurrentImageUrl(signedURL.url.toString()); + } catch (error) { + console.error("Error create song / file:", error); + } + } + + // Upload image, add to song, retrieve signed URL and retrieve the image. + // Also updates image if one already exists. + async function addNewImageToSong(e: React.ChangeEvent) { + + if (!currentSong) return; + + if (!e.target.files) return; + + const file = e.target.files[0]; + + try { + // Upload the Storage file: + const result = await uploadData({ + path: `images/${currentSong.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + // Add the file association to the record: + const response = await client.models.Song.update({ + id: currentSong.id, + coverArtPath: result?.path, + }); + + const updatedSong = response.data; + + setCurrentSong(updatedSong); + + // If the record has no associated file, we can return early. + if (!updatedSong?.coverArtPath) return; + + // Retrieve the file's signed URL: + const signedURL = await getUrl({ path: updatedSong.coverArtPath }); + setCurrentImageUrl(signedURL.url.toString()); + + } catch (error) { + console.error("Error uploading image / adding image to song: ", error); + } + } + + async function getImageForCurrentSong() { + if (!currentSong) return; + + try { + // Query the record to get the file path: + const response = await client.models.Song.get({ + id: currentSong.id, + }); + + const song = response.data; + + // If the record has no associated file, we can return early. + if (!song?.coverArtPath) return; + + // Retrieve the signed URL: + const signedURL = await getUrl({ path: song.coverArtPath }); + setCurrentImageUrl(signedURL.url.toString()); + } catch (error) { + console.error("Error getting song / image:", error); + } + } + + // Remove the file association, continue to persist both file and record + async function removeImageFromSong() { + + if (!currentSong) return; + + try { + const response = await client.models.Song.get({ + id: currentSong.id, + }); + + const song = response.data; + + // If the record has no associated file, we can return early. + if (!song?.coverArtPath) return; + + const updatedSong = await client.models.Song.update({ + id: song.id, + coverArtPath: null, + }); + + // If successful, the response here will be `null`: + setCurrentSong(updatedSong.data); + + setCurrentImageUrl(updatedSong.data?.coverArtPath); + + } catch (error) { + console.error("Error removing image from song: ", error); + } + } + + // Remove the record association and delete the file + async function deleteImageForCurrentSong() { + + if (!currentSong) return; + + try { + const response = await client.models.Song.get({ + id: currentSong.id, + }); + + const song = response?.data; + + // If the record has no associated file, we can return early. + if (!song?.coverArtPath) return; + + // Remove associated file from record + const updatedSong = await client.models.Song.update({ + id: song.id, + coverArtPath: null, + }); + + // Delete the file from S3: + await remove({ path: song.coverArtPath }); + + // If successful, the response here will be `null`: + setCurrentSong(updatedSong.data); + + setCurrentImageUrl(updatedSong.data?.coverArtPath); + + } catch (error) { + console.error("Error deleting image: ", error); + } + } + + // Delete both file and record + async function deleteCurrentSongAndImage() { + + if (!currentSong) return; + try { + const response = await client.models.Song.get({ + id: currentSong.id, + }); + const song = response.data; + + // If the record has no associated file, we can return early. + if (!song?.coverArtPath) return; + + await remove({ path: song.coverArtPath }); + + // Delete the record from the API: + await client.models.Song.delete({ id: song.id }); + + clearLocalState(); + + } catch (error) { + console.error("Error deleting song: ", error); + } + } + + function clearLocalState() { + setCurrentSong(null); + setCurrentImageUrl(""); + } + + return ( + <> +

Hello {user?.username}

+ +
+ + + + + + + +
+ + ); +} + +export default withAuthenticator(App); + + +``` +
+ + +```ts title="src/App.tsx" + +import "./App.css"; +import { generateClient } from "aws-amplify/api"; +import { uploadData, getUrl, remove } from "aws-amplify/storage"; +import React, { useState } from "react"; +import type { Schema } from "../amplify/data/resource"; +import "@aws-amplify/ui-react/styles.css"; +import { + type WithAuthenticatorProps, + withAuthenticator, +} from "@aws-amplify/ui-react"; +import { Amplify } from "aws-amplify"; +import outputs from "../amplify_outputs.json"; + +Amplify.configure(outputs); + +// Generating the client +const client = generateClient({ + authMode: "apiKey", +}); + +type PhotoAlbum = Schema["PhotoAlbum"]["type"]; + +function App({ signOut, user }: WithAuthenticatorProps) { + // State to hold the recognized text + const [currentPhotoAlbum, setCurrentPhotoAlbum] = useState( + null + ); + + // Used to display images for current photoAlbum: + const [currentImages, setCurrentImages] = useState< + (string | null | undefined)[] | null | undefined + >([]); + + async function createPhotoAlbumWithFirstImage( + e: React.ChangeEvent + ) { + if (!e.target.files) return; + + const file = e.target.files[0]; + + try { + // Create the API record: + const response = await client.models.PhotoAlbum.create({ + name: `My first photoAlbum`, + }); + + const photoAlbum = response.data; + + if (!photoAlbum) return; + + // Upload the Storage file: + const result = await uploadData({ + path: `images/${photoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + const updatePhotoAlbumDetails = { + id: photoAlbum.id, + imagePaths: [result.path], + }; + + // Add the file association to the record: + const updateResponse = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: [result.path], + }); + + const updatedPhotoAlbum = updateResponse.data; + + setCurrentPhotoAlbum(updatedPhotoAlbum); + + // If the record has no associated file, we can return early. + if (!updatedPhotoAlbum?.imagePaths?.length) return; + + // Retrieve the file's signed URL: + const signedURL = await getUrl({ + path: updatedPhotoAlbum.imagePaths[0]!, + }); + setCurrentImages([signedURL.url.toString()]); + } catch (error) { + console.error("Error create photoAlbum / file:", error); + } + } + + async function createPhotoAlbumWithMultipleImages( + e: React.ChangeEvent + ) { + if (!e.target.files) return; + + try { + const photoAlbumDetails = { + name: `My first photoAlbum`, + }; + + // Create the API record: + const response = await client.models.PhotoAlbum.create({ + name: `My first photoAlbum`, + }); + + const photoAlbum = response.data; + + if (!photoAlbum) return; + + // Upload all files to Storage: + const imagePaths = await Promise.all( + Array.from(e.target.files).map(async (file) => { + const result = await uploadData({ + path: `images/${photoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + return result.path; + }) + ); + + // Add the file association to the record: + const updateResponse = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: imagePaths, + }); + const updatedPhotoAlbum = updateResponse.data; + + setCurrentPhotoAlbum(updatedPhotoAlbum); + + // If the record has no associated file, we can return early. + if (!updatedPhotoAlbum?.imagePaths?.length) return; + + // Retrieve signed urls for all files: + const signedUrls = await Promise.all( + updatedPhotoAlbum.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) + ); + + if (!signedUrls) return; + setCurrentImages(signedUrls.map((signedUrl) => signedUrl.url.toString())); + } catch (error) { + console.error("Error create photoAlbum / file:", error); + } + } + + async function addNewImagesToPhotoAlbum( + e: React.ChangeEvent + ) { + if (!currentPhotoAlbum) return; + + if (!e.target.files) return; + + try { + // Upload all files to Storage: + const newimagePaths = await Promise.all( + Array.from(e.target.files).map(async (file) => { + const result = await uploadData({ + path: `images/${currentPhotoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + return result.path; + }) + ); + + // Query existing record to retrieve currently associated files: + const queriedResponse = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + + const photoAlbum = queriedResponse.data; + + if (!photoAlbum?.imagePaths) return; + + // Merge existing and new file paths: + const updatedimagePaths = [...newimagePaths, ...photoAlbum.imagePaths]; + + // Update record with merged file associations: + const response = await client.models.PhotoAlbum.update({ + id: currentPhotoAlbum.id, + imagePaths: updatedimagePaths, + }); + + const updatedPhotoAlbum = response.data; + setCurrentPhotoAlbum(updatedPhotoAlbum); + + // If the record has no associated file, we can return early. + if (!updatedPhotoAlbum?.imagePaths) return; + + // Retrieve signed urls for merged image paths: + const signedUrls = await Promise.all( + updatedPhotoAlbum?.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) + ); + + if (!signedUrls) return; + + setCurrentImages(signedUrls.map((signedUrl) => signedUrl.url.toString())); + } catch (error) { + console.error( + "Error uploading image / adding image to photoAlbum: ", + error + ); + } + } + + // Replace last image associated with current photoAlbum: + async function updateLastImage(e: React.ChangeEvent) { + if (!currentPhotoAlbum) return; + + if (!e.target.files) return; + + const file = e.target.files[0]; + + try { + // Upload new file to Storage: + const result = await uploadData({ + path: `images/${currentPhotoAlbum.id}-${file.name}`, + data: file, + options: { + contentType: "image/png", // contentType is optional + }, + }).result; + + const newFilePath = result.path; + + // Query existing record to retrieve currently associated files: + const queriedResponse = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + + const photoAlbum = queriedResponse.data; + + if (!photoAlbum?.imagePaths?.length) return; + + // Retrieve last image path: + const [lastImagePath] = photoAlbum.imagePaths.slice(-1); + + // Remove last file association by key + const updatedimagePaths = [ + ...photoAlbum.imagePaths.filter((key) => key !== lastImagePath), + newFilePath, + ]; + + // Update record with updated file associations: + const response = await client.models.PhotoAlbum.update({ + id: currentPhotoAlbum.id, + imagePaths: updatedimagePaths, + }); + + const updatedPhotoAlbum = response.data; + + setCurrentPhotoAlbum(updatedPhotoAlbum); + + // If the record has no associated file, we can return early. + if (!updatedPhotoAlbum?.imagePaths) return; + + // Retrieve signed urls for merged image paths: + const signedUrls = await Promise.all( + updatedPhotoAlbum?.imagePaths.map( + async (path) => await getUrl({ path: path! }) + ) + ); + + if (!signedUrls) return; + + setCurrentImages(signedUrls.map((signedUrl) => signedUrl.url.toString())); + } catch (error) { + console.error( + "Error uploading image / adding image to photoAlbum: ", + error + ); + } + } + + async function getImagesForPhotoAlbum() { + if (!currentPhotoAlbum) { + return; + } + try { + // Query the record to get the file paths: + const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + const photoAlbum = response.data; + + // If the record has no associated files, we can return early. + if (!photoAlbum?.imagePaths) return; + + // Retrieve the signed URLs for the associated images: + const signedUrls = await Promise.all( + photoAlbum.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + return await getUrl({ path: imagePath }); + }) + ); + + setCurrentImages( + signedUrls.map((signedUrl) => signedUrl?.url.toString()) + ); + } catch (error) { + console.error("Error getting photoAlbum / image:", error); + } + } + + // Remove the file associations, continue to persist both files and record + async function removeImagesFromPhotoAlbum() { + if (!currentPhotoAlbum) return; + + try { + const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + + const photoAlbum = response.data; + + // If the record has no associated file, we can return early. + if (!photoAlbum?.imagePaths) return; + + const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: null, + }); + + // If successful, the response here will be `null`: + setCurrentPhotoAlbum(updatedPhotoAlbum.data); + setCurrentImages(updatedPhotoAlbum.data?.imagePaths); + } catch (error) { + console.error("Error removing image from photoAlbum: ", error); + } + } + + // Remove the record association and delete the file + async function deleteImagesForCurrentPhotoAlbum() { + if (!currentPhotoAlbum) return; + + try { + const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + + const photoAlbum = response.data; + + // If the record has no associated files, we can return early. + if (!photoAlbum?.imagePaths) return; + + // Remove associated files from record + const updateResponse = await client.models.PhotoAlbum.update({ + id: photoAlbum.id, + imagePaths: null, // Set the file association to `null` + }); + + const updatedPhotoAlbum = updateResponse.data; + + // Delete the files from S3: + await Promise.all( + photoAlbum?.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + await remove({ path: imagePath }); + }) + ); + + // If successful, the response here will be `null`: + setCurrentPhotoAlbum(updatedPhotoAlbum); + setCurrentImages(null); + } catch (error) { + console.error("Error deleting image: ", error); + } + } + + // Delete both files and record + async function deleteCurrentPhotoAlbumAndImages() { + if (!currentPhotoAlbum) return; + + try { + const response = await client.models.PhotoAlbum.get({ + id: currentPhotoAlbum.id, + }); + + const photoAlbum = response.data; + + if (!photoAlbum) return; + + await client.models.PhotoAlbum.delete({ + id: photoAlbum.id, + }); + + setCurrentPhotoAlbum(null); + + // If the record has no associated file, we can return early. + if (!photoAlbum?.imagePaths) return; + + await Promise.all( + photoAlbum?.imagePaths.map(async (imagePath) => { + if (!imagePath) return; + await remove({ path: imagePath }); + }) + ); + + clearLocalState(); + } catch (error) { + console.error("Error deleting photoAlbum: ", error); + } + } + + function clearLocalState() { + setCurrentPhotoAlbum(null); + setCurrentImages([]); + } + + return ( +
+

Hello {user?.username}!

+

+ Current PhotoAlbum: {currentPhotoAlbum?.id} +

+ +
+ + + + + + + +
+ +
+ + + + + +
+ +
+ {currentImages && + currentImages.map((url, idx) => { + if (!url) return undefined; + return ( + Storage file + ); + })} +
+
+ ); +} + +export default withAuthenticator(App); + +``` +
+
+ + diff --git a/src/pages/[platform]/build-a-backend/server-side-rendering/index.mdx b/src/pages/[platform]/build-a-backend/server-side-rendering/index.mdx index 881cf06139c..56f3e58bae5 100644 --- a/src/pages/[platform]/build-a-backend/server-side-rendering/index.mdx +++ b/src/pages/[platform]/build-a-backend/server-side-rendering/index.mdx @@ -46,7 +46,7 @@ npm add aws-amplify @aws-amplify/adapter-nextjs You will need to create a `runWithAmplifyServerContextRunner` function to use Amplify APIs on the server-side of your Next.js app. -You can create an `amplifyServerUtils.ts` file under a `utils` folder in your codebase. In this file, you will import the Amplify configuration object from the `amplify_outputs.json` file that is generated by the Amplify CLI, and use the `createServerRunner` function to create the `runWithAmplifyServerContextRunner` function. +You can create an `amplifyServerUtils.ts` file under a `utils` folder in your codebase. In this file, you will import the Amplify backend outputs from the `amplify_outputs.json` file that is generated by the Amplify CLI, and use the `createServerRunner` function to create the `runWithAmplifyServerContextRunner` function. For example, the `utils/amplifyServerUtils.ts` file may contain the following content: @@ -55,7 +55,7 @@ import { createServerRunner } from '@aws-amplify/adapter-nextjs'; import outputs from '@/amplify_outputs.json'; export const { runWithAmplifyServerContext } = createServerRunner({ - config + outputs }); ``` @@ -81,7 +81,7 @@ When you use the Amplify library on the client-side of your Next.js app, you wil import outputs from '@/amplify_outputs.json'; import { Amplify } from 'aws-amplify'; -Amplify.configure(config, { +Amplify.configure(outputs, { ssr: true // required when using Amplify with Next.js }); @@ -112,7 +112,7 @@ If you're using the Next.js App Router, you can create a client component to con import { Amplify } from 'aws-amplify'; import outputs from '../amplify_outputs.json'; -Amplify.configure(config, { ssr: true }); +Amplify.configure(outputs, { ssr: true }); export default function ConfigureAmplifyClientSide() { return null; diff --git a/src/pages/[platform]/build-a-backend/server-side-rendering/nextjs-app-router-server-components/index.mdx b/src/pages/[platform]/build-a-backend/server-side-rendering/nextjs-app-router-server-components/index.mdx index 7b0c9c66e9c..b225f35fdc2 100644 --- a/src/pages/[platform]/build-a-backend/server-side-rendering/nextjs-app-router-server-components/index.mdx +++ b/src/pages/[platform]/build-a-backend/server-side-rendering/nextjs-app-router-server-components/index.mdx @@ -63,7 +63,7 @@ import { Amplify } from "aws-amplify"; import outputs from "@/amplify_outputs.json"; -Amplify.configure(config, { ssr: true }); +Amplify.configure(outputs, { ssr: true }); export default function ConfigureAmplifyClientSide() { return null; @@ -126,7 +126,7 @@ import { type Schema } from "@/amplify/data/resource"; import outputs from "@/amplify_outputs.json"; export const { runWithAmplifyServerContext } = createServerRunner({ - config, + outputs, }); export const cookiesClient = generateServerClientUsingCookies({ diff --git a/src/pages/[platform]/build-a-backend/storage/data-usage/index.mdx b/src/pages/[platform]/build-a-backend/storage/data-usage/index.mdx index d9679694ddc..4c228f8f8d5 100644 --- a/src/pages/[platform]/build-a-backend/storage/data-usage/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/data-usage/index.mdx @@ -3,7 +3,8 @@ import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; export const meta = { title: 'Data usage policy', description: "Review the data types gathered by the Amplify library that Apple requires you to disclose in your app's data usage policy when submitting the app to the App Store.", - platforms: ['swift'] + platforms: ['swift', + 'flutter'] }; export const getStaticPaths = async () => { diff --git a/src/pages/[platform]/build-a-backend/storage/download-files/index.mdx b/src/pages/[platform]/build-a-backend/storage/download-files/index.mdx index 9ad79a65a56..b886295a428 100644 --- a/src/pages/[platform]/build-a-backend/storage/download-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/download-files/index.mdx @@ -188,7 +188,6 @@ let url = try await Amplify.Storage.getURL( Option | Type | Description | | -- | -- | ----------- | | expires | Integer | Number of seconds before the URL expires | -| pluginOptions | - | Extra plugin specific options, only used in special circumstances when the existing options do not provide a way to utilize the underlying storage system's functionality. | @@ -368,16 +367,6 @@ let resultSink = downloadTask - - -### More download options - -Option | Description | -| -- | ----------- | -| pluginOptions | Extra plugin specific options, only used in special circumstances when the existing options do not provide a way to utilize the underlying storage system's functionality. | - - - You can download a file to a local directory using `Amplify.Storage.downloadFile`. diff --git a/src/pages/[platform]/build-a-backend/storage/list-files/index.mdx b/src/pages/[platform]/build-a-backend/storage/list-files/index.mdx index 0a7d1697bdd..be6f2ed2270 100644 --- a/src/pages/[platform]/build-a-backend/storage/list-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/list-files/index.mdx @@ -290,7 +290,6 @@ Option | Type | Description | | -- | -- | ----------- | | pageSize | UInt | Number between 1 and 1,000 that indicates the limit of how many entries to fetch when retrieving file lists from the server | | nextToken | String | String indicating the page offset at which to resume a listing. | -| pluginOptions | - | Extra plugin specific options, only used in special circumstances when the existing options do not provide a way to utilize the underlying storage system's functionality. | diff --git a/src/pages/[platform]/build-a-backend/storage/remove-files/index.mdx b/src/pages/[platform]/build-a-backend/storage/remove-files/index.mdx index a07afbd9e45..f468563172f 100644 --- a/src/pages/[platform]/build-a-backend/storage/remove-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/remove-files/index.mdx @@ -134,12 +134,6 @@ receiveValue: { removedObject in -### More `remove` options - -Option | Description | -| -- | ----------- | -| pluginOptions | Extra plugin specific options, only used in special circumstances when the existing options do not provide a way to utilize the underlying storage system's functionality. | - diff --git a/src/pages/[platform]/build-a-backend/storage/set-up-storage/index.mdx b/src/pages/[platform]/build-a-backend/storage/set-up-storage/index.mdx index b20ffce3acb..cc9ad4de834 100644 --- a/src/pages/[platform]/build-a-backend/storage/set-up-storage/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/set-up-storage/index.mdx @@ -53,7 +53,7 @@ export const storage = defineStorage({ }); ``` -Import your storage definition in your `amplify/backend.ts` file that contains your backend definition. Add storage to `defineBackend`. +Import your storage definition in your `amplify/backend.ts` file that contains your backend definition. Add storage to `defineBackend`. ```ts title="amplify/backend.ts" import { defineBackend } from '@aws-amplify/backend'; @@ -81,12 +81,12 @@ git push By default, no users or other project resources have access to any files in the storage bucket. Access must be explicitly granted within `defineStorage` using the `access` callback. -The access callback returns an object where each key in the object is a file path and each value in the object is an array of access rules that apply to that path. +The access callback returns an object where each key in the object is a file path and each value in the object is an array of access rules that apply to that path. -The following example shows you how you can set up your file storage structure for a generic photo sharing app. Here, +The following example shows you how you can set up your file storage structure for a generic photo sharing app. Here, 1. Guests have access to see all profile pictures and only the users that uploaded the profile picture can replace or delete them. Users are identified by their Identity Pool ID in this case i.e. identityID. -2. There's also a general pool where all users can submit pictures. +2. There's also a general pool where all users can submit pictures. [Learn more about customizing access to file path](/[platform]/build-a-backend/storage/authorization/). @@ -148,7 +148,7 @@ For a full example, please follow the [project setup walkthrough](/[platform]/st {/* is the visionOS support callout relevant here? */} -visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. +visionOS support is currently in **preview** and can be used by targeting the [`visionos-preview`](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview) branch. As new Xcode 15 beta versions are released, the branch will be updated with any necessary fixes on a best effort basis. For more information on how to use the `visionos-preview` branch, see [Platform Support](https://github.com/aws-amplify/amplify-swift/tree/visionos-preview#platform-support). @@ -191,11 +191,11 @@ struct MyAmplifyApp: App { } } - init() { + init() { do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSS3StoragePlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with Auth and Storage plugins") } catch { print("Failed to initialize Amplify with \(error)") @@ -226,7 +226,7 @@ func application( do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSS3StoragePlugin()) - try Amplify.configure() + try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with Auth and Storage plugins") } catch { print("Failed to initialize Amplify with \(error)") @@ -277,6 +277,16 @@ Initialize Amplify Storage by calling `Amplify.addPlugin()`. To complete initial Add the following code to your `onCreate()` method in your application class: + +Before calling the `Amplify.configure` function, make sure to either download the `amplify_outputs.json` file from the console, or generate it with the following command: + +```bash title="Terminal" showLineNumbers={false} +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw +``` + +Next, be sure the file you generated or downloaded is in the appropriate resource directory for your application (for example, `app/src/main/res/raw`) in your Android project. Otherwise, you will not be able to compile your application. + + @@ -284,6 +294,7 @@ Add the following code to your `onCreate()` method in your application class: import android.util.Log; import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; import com.amplifyframework.core.Amplify; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.storage.s3.AWSS3StoragePlugin; ``` @@ -304,7 +315,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSS3StoragePlugin plugins Amplify.addPlugin(new AWSCognitoAuthPlugin()); Amplify.addPlugin(new AWSS3StoragePlugin()); - Amplify.configure(getApplicationContext()); + Amplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -321,6 +332,7 @@ public class MyAmplifyApp extends Application { import android.util.Log import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.core.Amplify +import com.amplifyframework.core.configuration.AmplifyOutputs import com.amplifyframework.storage.s3.AWSS3StoragePlugin ``` @@ -340,8 +352,7 @@ class MyAmplifyApp : Application() { // Add these lines to add the AWSCognitoAuthPlugin and AWSS3StoragePlugin plugins Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSS3StoragePlugin()) - Amplify.configure(applicationContext) - + Amplify.configure(AmplifyOutputs(R.raw.amplify_outputs), applicationContext) Log.i("MyAmplifyApp", "Initialized Amplify") } catch (error: AmplifyException) { Log.e("MyAmplifyApp", "Could not initialize Amplify", error) @@ -356,6 +367,7 @@ class MyAmplifyApp : Application() { ```java import android.util.Log; import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin; +import com.amplifyframework.core.configuration.AmplifyOutputs; import com.amplifyframework.rx.RxAmplify; import com.amplifyframework.storage.s3.AWSS3StoragePlugin; ``` @@ -377,7 +389,7 @@ public class MyAmplifyApp extends Application { // Add these lines to add the AWSCognitoAuthPlugin and AWSS3StoragePlugin plugins RxAmplify.addPlugin(new AWSCognitoAuthPlugin()); RxAmplify.addPlugin(new AWSS3StoragePlugin()); - RxAmplify.configure(getApplicationContext()); + RxAmplify.configure(AmplifyOutputs.fromResource(R.raw.amplify_outputs), getApplicationContext()); Log.i("MyAmplifyApp", "Initialized Amplify"); } catch (AmplifyException error) { @@ -520,8 +532,8 @@ upload.addEventListener("click", () => { console.log("Complete File read successfully!", event.target.result); try { await uploadData({ - data: event.target.result, - path: `picture-submissions/${file.files[0].name}` + data: event.target.result, + path: `picture-submissions/${file.files[0].name}` }); } catch (e) { console.log("error", e); diff --git a/src/pages/[platform]/build-a-backend/storage/upload-files/index.mdx b/src/pages/[platform]/build-a-backend/storage/upload-files/index.mdx index 8a765976083..a0b53b88bc4 100644 --- a/src/pages/[platform]/build-a-backend/storage/upload-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/storage/upload-files/index.mdx @@ -1156,7 +1156,6 @@ Option | Type | Description | | -- | -- | ----------- | | metadata | Dictionary | Metadata for the object to store. | | contentType | String | The standard MIME type describing the format of the object to store. | -| pluginOptions | - | Extra plugin specific options, only used in special circumstances when the existing options do not provide a way to utilize the underlying storage system's functionality. | diff --git a/src/pages/[platform]/build-ui/formbuilder/lifecycle/index.mdx b/src/pages/[platform]/build-ui/formbuilder/lifecycle/index.mdx index bc977dffb03..3eb1a8be997 100644 --- a/src/pages/[platform]/build-ui/formbuilder/lifecycle/index.mdx +++ b/src/pages/[platform]/build-ui/formbuilder/lifecycle/index.mdx @@ -58,7 +58,7 @@ Hook into the form's lifecycle events to customize user input before submission, ## Get form data as your user inputs data - onChange -In some cases, you want to get the form data in real-time as the user is filling the form. The `onChange` event provides you the form data in the `fields` parameter. We recommend you to use `onChange` if you opted to [hide all form action buttons](/gen1/[platform]/build-ui/formbuilder/call-to-action/#toggle-visibility-for-submit-cancel-clear-and-reset-buttons) (Submit, Cancel, Clear, Reset). +In some cases, you want to get the form data in real-time as the user is filling the form. The `onChange` event provides you the form data in the `fields` parameter. ```jsx import { useState } from 'react' @@ -75,7 +75,7 @@ function App() { ## Extend validation rules in code - onValidate -With the `onValidate` event, you can extend the validation rules in code. Learn more about [How to add validation rules](/gen1/[platform]/build-ui/formbuilder/validations/). +With the `onValidate` event, you can extend the validation rules in code. Learn more about [How to add validation rules](/[platform]/build-ui/formbuilder/validations/). ## Handle form data submissions - onSubmit diff --git a/src/pages/[platform]/build-ui/formbuilder/special-inputs/index.mdx b/src/pages/[platform]/build-ui/formbuilder/special-inputs/index.mdx index 04691236c22..10648a52901 100644 --- a/src/pages/[platform]/build-ui/formbuilder/special-inputs/index.mdx +++ b/src/pages/[platform]/build-ui/formbuilder/special-inputs/index.mdx @@ -28,12 +28,12 @@ export function getStaticProps(context) { ## Storage Manager -[**Storage Manager**](https://ui.docs.amplify.aws/react/connected-components/storage/storagemanager) fields allow your forms to accept file uploads, which are stored in an Amazon S3 bucket connected to your Amplify app. After uploading, that file's S3 key is stored in your data model, allowing for systematic retrieval using the [Amplify JS library](/gen1/[platform]/build-a-backend/storage/download/). +[**Storage Manager**](https://ui.docs.amplify.aws/react/connected-components/storage/storagemanager) fields allow your forms to accept file uploads, which are stored in an Amazon S3 bucket connected to your Amplify app. After uploading, that file's S3 key is stored in your data model, allowing for systematic retrieval using the [Amplify JS library](/[platform]/build-a-backend/storage/download-files/). ### Prerequisites -In order to use the Storage Manager field, your Amplify app must have an Amplify app with [Authentication](react/build-a-backend/auth/set-up-auth/) and [Storage](react/build-a-backend/storage/define-storage/) enabled. +In order to use the Storage Manager field, your Amplify app must have an Amplify app with [Authentication](/[platform]/build-a-backend/auth/set-up-auth/) and [Storage](/[platform]/build-a-backend/storage/set-up-storage/) enabled. ### How it works diff --git a/src/pages/[platform]/deploy-and-host/fullstack-branching/branch-deployments/index.mdx b/src/pages/[platform]/deploy-and-host/fullstack-branching/branch-deployments/index.mdx index 5c0b2a51995..fc700488fcd 100644 --- a/src/pages/[platform]/deploy-and-host/fullstack-branching/branch-deployments/index.mdx +++ b/src/pages/[platform]/deploy-and-host/fullstack-branching/branch-deployments/index.mdx @@ -91,7 +91,7 @@ npx ampx generate outputs --app-id --branch main --out-dir
```bash title="Terminal" showLineNumbers={false} -npx ampx generate config --app-id --branch main --out-dir app/src/main/res/raw +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw ``` diff --git a/src/pages/[platform]/deploy-and-host/fullstack-branching/cross-account-deployments/index.mdx b/src/pages/[platform]/deploy-and-host/fullstack-branching/cross-account-deployments/index.mdx index accf46a99ab..d7d3e51a162 100644 --- a/src/pages/[platform]/deploy-and-host/fullstack-branching/cross-account-deployments/index.mdx +++ b/src/pages/[platform]/deploy-and-host/fullstack-branching/cross-account-deployments/index.mdx @@ -53,7 +53,7 @@ Please refer to this Amazon CodeCatalyst [guide](https://docs.aws.amazon.com/cod ### Step 3: Update build specification -Add the `npx ampx generate outputs --branch $AWS_BRANCH --app-id $AWS_APP_ID` command to the build spec and comment out the `npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID` command. `ampx pipeline-deploy` runs a script to deploy backend updates, while `amplify generate config` fetches the latest `amplify_outputs.json` for the specified environment. +Add the `npx ampx generate outputs --branch $AWS_BRANCH --app-id $AWS_APP_ID` command to the build spec and comment out the `npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID` command. `ampx pipeline-deploy` runs a script to deploy backend updates, while `ampx generate config` fetches the latest `amplify_outputs.json` for the specified environment. ![Screenshot of Build image settings section in AWS Amplify Gen 2 console, with details about the app build specification](images/gen2/cross-account-deployments/pipeline10.png) diff --git a/src/pages/[platform]/deploy-and-host/fullstack-branching/custom-pipelines/index.mdx b/src/pages/[platform]/deploy-and-host/fullstack-branching/custom-pipelines/index.mdx index 40614668ffc..edf9a2e7ed3 100644 --- a/src/pages/[platform]/deploy-and-host/fullstack-branching/custom-pipelines/index.mdx +++ b/src/pages/[platform]/deploy-and-host/fullstack-branching/custom-pipelines/index.mdx @@ -43,7 +43,7 @@ You can set up your backend deployments using the following steps: -3. Update the Amplify build-spec to add `npx ampx generate outputs --branch $AWS_BRANCH --app-id $AWS_APP_ID` and comment out the `pipeline-deploy` script. `ampx pipeline-deploy` runs a script to deploy backend updates, while `amplify generate config` fetches the latest `amplify_outputs.json` for the specified environment. +3. Update the Amplify build-spec to add `npx ampx generate outputs --branch $AWS_BRANCH --app-id $AWS_APP_ID` and comment out the `pipeline-deploy` script. `ampx pipeline-deploy` runs a script to deploy backend updates, while `ampx generate config` fetches the latest `amplify_outputs.json` for the specified environment. ![custom-ci](/images/gen2/fullstack-branching/custom-ci.png) diff --git a/src/pages/[platform]/deploy-and-host/sandbox-environments/features/index.mdx b/src/pages/[platform]/deploy-and-host/sandbox-environments/features/index.mdx index c143ee7e1f6..1fae0711111 100644 --- a/src/pages/[platform]/deploy-and-host/sandbox-environments/features/index.mdx +++ b/src/pages/[platform]/deploy-and-host/sandbox-environments/features/index.mdx @@ -234,7 +234,7 @@ npx ampx generate config --app-id --branch main --format dart --out-dir ```bash title="Terminal" showLineNumbers={false} -npx ampx generate config --app-id --branch main --format json-mobile --out-dir app/src/main/res/raw +npx ampx generate outputs --app-id --branch main --out-dir app/src/main/res/raw ``` diff --git a/src/pages/[platform]/index.tsx b/src/pages/[platform]/index.tsx index 9d568f0cacf..6485de157b6 100644 --- a/src/pages/[platform]/index.tsx +++ b/src/pages/[platform]/index.tsx @@ -53,6 +53,10 @@ export function getStaticProps() { const Gen2Overview = () => { const currentPlatform = useCurrentPlatform(); + const isMobilePlatform = ['swift', 'android', 'flutter'].includes( + currentPlatform + ); + const isFlutter = currentPlatform == 'flutter'; return ( @@ -60,12 +64,29 @@ const Gen2Overview = () => { Amplify Documentation for {PLATFORM_DISPLAY_NAMES[currentPlatform]} - - AWS Amplify is everything frontend developers need to develop and - deploy cloud-powered fullstack applications without hassle. Easily - connect your frontend to the cloud for data modeling, authentication, - storage, serverless functions, SSR app deployment, and more. - + {isFlutter ? ( + + AWS Amplify is everything Flutter developers need to develop + cloud-powered fullstack applications without hassle. Easily connect + your Flutter applications to the cloud for data modeling, + authentication, storage, serverless functions, and more. + + ) : isMobilePlatform ? ( + + AWS Amplify is everything mobile developers need to develop + cloud-powered fullstack applications without hassle. Easily connect + your cross-platform applications to the cloud for data modeling, + authentication, storage, serverless functions, and more. + + ) : ( + + AWS Amplify is everything frontend developers need to develop and + deploy cloud-powered fullstack applications without hassle. Easily + connect your frontend to the cloud for data modeling, + authentication, storage, serverless functions, SSR app deployment, + and more. + + )} { Features - - - - Code-first DX - - - The fullstack TypeScript developer experience lets you focus on - your app code instead of infrastructure. - - - + {isMobilePlatform ? ( + + + + Code-first DX + + + The new code-first developer experience lets you define your + infrastructure with TypeScript. + + + + ) : ( + + + + Code-first DX + + + The fullstack TypeScript developer experience lets you focus + on your app code instead of infrastructure. + + + + )} @@ -139,47 +174,66 @@ const Gen2Overview = () => { Develop - @@ -222,18 +278,20 @@ const Gen2Overview = () => { /> - - Deploy Next.js, Nuxt, React, Vue.js, Angular (and more) apps by - simply connecting your Git repository. - + {!isMobilePlatform && ( + + Deploy Next.js, Nuxt, React, Vue.js, Angular (and more) apps by + simply connecting your Git repository. + + )} @@ -1616,7 +1608,7 @@ Run your application in your local environment again. You should be presented wi ## Adding Data -The initial scaffolding already has a pre-configured data backend defined in the `amplify/data/resource`.ts file. The default example will create a Todo model with `content` field. +The initial scaffolding already has a pre-configured data backend defined in the `amplify/data/resource.ts` file. The default example will create a Todo model with `content` field. Let's modify this to add the following: - A boolean `isDone` field. @@ -1630,7 +1622,7 @@ const schema = a.schema({ Todo: a .model({ content: a.string(), - isDone: a.boolean() + isDone: a.boolean().required() }) .authorization((allow) => [allow.owner()]) }); @@ -1674,89 +1666,109 @@ init() { } ``` -Update the `createTodo` function in the `MyAmplifyAppApp.swift` file with the following code: +Create a new file called `TodoViewModel.swift` and the `createTodo` function the following code: -```swift title="MyAmplifyAppApp.swift" -func createTodo() async { - let creationTime = Temporal.DateTime.now() - let todo = Todo( - content: "Random Todo \(creationTime)", - isDone: false, - createdAt: creationTime, - updatedAt: creationTime - ) - do { - let result = try await Amplify.API.mutate(request: .create(todo)) - switch result { - case .success(let todo): - print("Successfully created todo: \(todo)") - case .failure(let error): - print("Got failed result with \(error.errorDescription)") +```swift title="TodoViewModel.swift" +import Amplify + +@MainActor +class TodoViewModel: ObservableObject { + func createTodo() { + let creationTime = Temporal.DateTime.now() + let todo = Todo( + content: "Random Todo \(creationTime)", + isDone: false, + createdAt: creationTime, + updatedAt: creationTime + ) + Task { + do { + let result = try await Amplify.API.mutate(request: .create(todo)) + switch result { + case .success(let todo): + print("Successfully created todo: \(todo)") + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to create todo: ", error) + } catch { + print("Unexpected error: \(error)") + } } - } catch let error as APIError { - print("Failed to create todo: ", error) - } catch { - print("Unexpected error: \(error)") } } + ``` The code above will create a random todo with the current time. -Next, update the `listTodos` function in the `MyAmplifyAppApp.swift` for listing to-do items: +Next, update the `listTodos` function in the `TodoViewModel.swift` for listing to-do items: -```swift title="MyAmplifyAppApp.swift" -func listTodos() async { - let request = GraphQLRequest.list(Todo.self) - do { - let result = try await Amplify.API.query(request: request) - switch result { - case .success(let todos): - self.todos = todos.elements - print("Successfully retrieved list of todos: \(todos)") - case .failure(let error): - print("Got failed result with \(error.errorDescription)") +```swift title="TodoViewModel.swift" +@MainActor +class TodoViewModel: ObservableObject { + @Published var todos: [Todo] = [] + + func createTodo() { + /// ... + } + + func listTodos() { + let request = GraphQLRequest.list(Todo.self) + Task { + do { + let result = try await Amplify.API.query(request: request) + switch result { + case .success(let todos): + print("Successfully retrieved list of todos: \(todos)") + self.todos = todos.elements + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to query list of todos: ", error) + } catch { + print("Unexpected error: \(error)") + } } - } catch let error as APIError { - print("Failed to query list of todos: ", error) - } catch { - print("Unexpected error: \(error)") } } -``` -This will assign the value of the fetched todos into a State object. Be sure to create it before the `body` property: -```swift -@State var todos: [Todo] = [] ``` -Now let's update the UI code to show the todos. Update the `VStack` in the `MyAmplifyAppApp.swift` file with the following code: +This will assign the value of the fetched todos into a Published object. -```swift -VStack { - Button("Sign out") { - Task { - await state.signOut() - } - } - List(todos, id: \.id) { todo in - Text(todo.content!) - } - Button(action: { - Task { - await createTodo() - await listTodos() - } - }) { - HStack { - Text("Add a New Todo") - Image(systemName: "plus") - } - } - .accessibilityLabel("New Todo") -}.task { - await listTodos() +Now let's update the UI code to observe the todos. Update the `VStack` in the `ContentView.swift` file with the following code: + +```swift title="ContentView.swift" +struct ContentView: View { + + // Create an observable object instance. + @StateObject var vm = TodoViewModel() + + var body: some View { + Authenticator { state in + VStack { + Button("Sign out") { + Task { + await state.signOut() + } + } + Button(action: { + vm.createTodo() + vm.listTodos() + }) { + HStack { + Text("Add a New Todo") + Image(systemName: "plus") + } + } + .accessibilityLabel("New Todo") + } + } + } } ``` @@ -1767,79 +1779,113 @@ VStack { [documentation](https://developer.apple.com/documentation/swift/task). -The code above will fetch the todos once the VStack is shown. It will also create a todo and update the todo list each time a todo is created. +The code will create a todo and update the todo list each time a todo is created. -Next step is to update and delete the todos. For that, create `updateTodo` and `deleteTodo` functions in the `MyAmplifyAppApp.swift` file with the following code: +Next step is to update and delete the todos. For that, create `updateTodo` and `deleteTodo` functions in the `TodoViewModel.swift` file with the following code: -```swift -func deleteTodo(todo: Todo) async { - do { - let result = try await Amplify.API.mutate(request: .delete(todo)) - switch result { - case .success(let todo): - print("Successfully deleted todo: \(todo)") - case .failure(let error): - print("Got failed result with \(error.errorDescription)") +```swift title="TodoViewModel.swift" +@MainActor +class TodoViewModel: ObservableObject { + @Published var todos: [Todo] = [] + + func createTodo() { + // ... + } + + func listTodos() { + // ... + } + + func deleteTodos(indexSet: IndexSet) { + for index in indexSet { + let todo = todos[index] + Task { + do { + let result = try await Amplify.API.mutate(request: .delete(todo)) + switch result { + case .success(let todo): + print("Successfully deleted todo: \(todo)") + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to deleted todo: ", error) + } catch { + print("Unexpected error: \(error)") + } + } } - } catch let error as APIError { - print("Failed to deleted todo: ", error) - } catch { - print("Unexpected error: \(error)") } -} -func updateTodo(todo: Todo) async { - do { - let result = try await Amplify.API.mutate(request: .update(todo)) - switch result { - case .success(let todo): - print("Successfully updated todo: \(todo)") - case .failure(let error): - print("Got failed result with \(error.errorDescription)") + func updateTodo(todo: Todo) { + Task { + do { + let result = try await Amplify.API.mutate(request: .update(todo)) + switch result { + case .success(let todo): + print("Successfully updated todo: \(todo)") + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to updated todo: ", error) + } catch { + print("Unexpected error: \(error)") + } } - } catch let error as APIError { - print("Failed to updated todo: ", error) - } catch { - print("Unexpected error: \(error)") } } + ``` -Lastly, update the `List` in the `MyAmplifyAppApp.swift` file with the following code: +Update the `List` in the `ContentView.swift` file with the following code: -```swift -List(todos, id: \.id) { todo in - @State var isToggled = todo.isDone! - Toggle(isOn: $isToggled - ) { - Text(todo.content!) - }.onTapGesture { - var updatedTodo = todos.first {$0.id == todo.id}! - updatedTodo.isDone = !todo.isDone! - Task { - await updateTodo(todo: updatedTodo) - await listTodos() +```swift title="ContentView.swift" +struct ContentView: View { + @StateObject var vm = TodoViewModel() + + var body: some View { + Authenticator { state in + VStack { + // ... Sign out Button + List { + ForEach($vm.todos, id: \.id) { todo in + TodoRow(vm: vm, todo: todo) + }.onDelete { indexSet in + vm.deleteTodos(indexSet: indexSet) + vm.listTodos() + } + } + // ... Add new Todo button + } } } - .onChange(of: isToggled) { oldValue, newValue in - var updatedTodo = todos.first {$0.id == todo.id}! - updatedTodo.isDone = newValue - Task { - await updateTodo(todo: updatedTodo) - await listTodos() +} +``` + +Lastly, create a new file called `TodoRow.swift` with the following code: + +```swift title="TodoRow.swift" +struct TodoRow: View { + @ObservedObject var vm: TodoViewModel + @Binding var todo: Todo + + var body: some View { + Toggle(isOn: $todo.isDone) { + Text(todo.content ?? "") } - } - .toggleStyle(.switch) - .onLongPressGesture { - Task { - await deleteTodo(todo: todo) - await listTodos() + .toggleStyle(SwitchToggleStyle()) + .onChange(of: todo.isDone) { _, newValue in + var updatedTodo = todo + updatedTodo.isDone = newValue + vm.updateTodo(todo: updatedTodo) + vm.listTodos() } } } ``` -This will update the UI to show a toggle to update the todo and a long press gesture to delete the todo. Now if you run the application you should see the following flow. +This will update the UI to show a toggle to update the todo `isDone` and a swipe to delete the todo. Now if you run the application you should see the following flow. diff --git a/src/pages/gen1/[platform]/build-a-backend/storage/path/index.mdx b/src/pages/gen1/[platform]/build-a-backend/storage/path/index.mdx index 23f9c4627a8..e99205a0874 100644 --- a/src/pages/gen1/[platform]/build-a-backend/storage/path/index.mdx +++ b/src/pages/gen1/[platform]/build-a-backend/storage/path/index.mdx @@ -42,7 +42,8 @@ export function getStaticProps(context) { You can now use the `path` parameter in the API to access any path within your S3 bucket. This provides more flexibility than the predefined `protected`, `private`, or `guest` folder access levels, allowing you to create and manage a storage structure tailored to your needs. -Note : `path` parameter can not be empty or start with a '/' (leading slash) +Note: `path` parameter can not be empty or start with a '/' (leading slash) + The sections below explain how to use existing `guest`, `protected`, and `private` resources with path-based APIs. ## Using Guest accessLevel diff --git a/src/pages/gen1/[platform]/tools/console/authz/permissions/index.mdx b/src/pages/gen1/[platform]/tools/console/authz/permissions/index.mdx index 55ffd5832bf..28c1372be93 100644 --- a/src/pages/gen1/[platform]/tools/console/authz/permissions/index.mdx +++ b/src/pages/gen1/[platform]/tools/console/authz/permissions/index.mdx @@ -71,4 +71,4 @@ If you want your data model to be publicly accessible, switch to API_KEY or IAM 1. Using the *Books* data model that you created in the [Create a data model example](/gen1/[platform]/tools/console/data/data-model/#Create-a-data-model-example), set the authorization mode to **API Key**. 2. In the **Model** pane on the right, expand the **Anyone** window. Choose **Read** to specify that any signed in user has read access to the data in the *Book* model. -![](/images/console/7_publicauthreadonly.png) +![](/images/console/7_authreadonly.png) diff --git a/tasks/console-errors.js b/tasks/console-errors.js new file mode 100644 index 00000000000..cdc750fe460 --- /dev/null +++ b/tasks/console-errors.js @@ -0,0 +1,109 @@ +const puppeteer = require('puppeteer'); // eslint-disable-line +const { getSitemapUrls } = require('./get-sitemap-links'); // eslint-disable-line + +// Here we are excluding shortbread errors because these are domain specific and are expected to fail in a local environment +const excludedErrors = [ + { + type: 'Shortbread', + errorText: + "Shortbread failed to set user's cookie preference because the domain name that was passed" + } +]; + +const excludedScripts = [ + 'prod.assets.shortbread.aws', + 'prod.tools.shortbread.aws', + 'prod.tools.shortbread.aws.dev', + 'aa0.awsstatic.com', + 'alpha.d2c.marketing.aws.dev', + 'vs-alpha.aws.amazon.com' +]; + +const LOCALHOST = 'http://localhost:3000'; + +const checkPage = async (url) => { + const errorsFound = []; + const browser = await puppeteer.launch({ headless: 'new' }); + + const page = await browser.newPage(); + + await page.setRequestInterception(true); + + page + .on('pageerror', (message) => { + const errorText = message.message; + const excluded = excludedErrors.some((excludedError) => { + return errorText.includes(excludedError.errorText); + }); + + if (!excluded) { + errorsFound.push({ + page: url, + message: errorText + }); + } + }) + .on('console', (message) => { + if (message.type().toLowerCase() === 'error') { + const errorText = message.text(); + const callingScript = message.location().url; + const excludedFromError = excludedErrors.some((excludedError) => { + return errorText.includes(excludedError.errorText); + }); + const excludedFromScript = excludedScripts.some((excludedScript) => { + return callingScript.includes(excludedScript); + }); + const excluded = excludedFromError || excludedFromScript; + + if (!excluded) { + errorsFound.push({ + page: url, + message: errorText, + stackTrace: message.stackTrace() + }); + } + } + }) + + .on('request', (interceptedRequest) => { + const excludedFromScript = excludedScripts.some((excludedScript) => { + return interceptedRequest.url().includes(excludedScript); + }); + if (excludedFromScript) { + interceptedRequest.abort(); + } else interceptedRequest.continue(); + }); + + await page.goto(url, { waitUntil: 'domcontentloaded' }); + + await browser.close(); + + return errorsFound; +}; + +const consoleErrors = async (domain) => { + const pagesToCheck = await getSitemapUrls(domain); + let errorMessage = ''; + for (let i = 0; i < pagesToCheck.length; i++) { + const url = pagesToCheck[i]; + console.log(`checking page ${url}`); + const errorsFound = await checkPage(url); + errorsFound.forEach((error) => { + errorMessage += `${error.message} found on ${error.page}\n`; + }); + } + if (errorMessage != '') { + console.log( + `--------- Console errors have been found and need to be resolved in order to merge. ------- + ------- Please note that these errors could be on pages that were not edited in this PR. ---------` + ); + console.log(errorMessage); + } + return errorMessage; +}; + +module.exports = { + consoleErrors: async (domain = LOCALHOST) => { + return await consoleErrors(domain); + } +}; diff --git a/tasks/get-sitemap-links.js b/tasks/get-sitemap-links.js new file mode 100644 index 00000000000..8e40ac783a2 --- /dev/null +++ b/tasks/get-sitemap-links.js @@ -0,0 +1,42 @@ +const puppeteer = require('puppeteer'); // eslint-disable-line + +const DOMAIN = 'https://docs.amplify.aws'; +const SITEMAP_URL = 'https://docs.amplify.aws/sitemap.xml'; + +const getSitemapUrls = async (localDomain) => { + let browser = await puppeteer.launch({ headless: 'new' }); + + const page = await browser.newPage(); + + let siteMap = localDomain ? `${localDomain}/sitemap.xml` : SITEMAP_URL; + let response = await page.goto(siteMap); + + const siteMapUrls = []; + + if (response && response.status() && response.status() === 200) { + const urlTags = await page.evaluateHandle(() => { + return document.getElementsByTagName('loc'); + }); + const numOfLinks = await page.evaluate((e) => e.length, urlTags); + for (let i = 0; i < numOfLinks; i++) { + let url = await page.evaluate( + (urlTags, i) => urlTags[i].innerHTML, + urlTags, + i + ); + if (localDomain) { + // Currently the sitemap is always generated with the prod docs domain so we need to replace this with localhost + url = url.replace(DOMAIN, localDomain); + } + siteMapUrls.push(url); + } + } + browser.close(); + return siteMapUrls; +}; + +module.exports = { + getSitemapUrls: async (domain) => { + return await getSitemapUrls(domain); + } +}; diff --git a/yarn.lock b/yarn.lock index 2dec273099d..33e00175b74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5105,10 +5105,10 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ejs@^3.1.7: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== +ejs@3.1.10, ejs@^3.1.7: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" @@ -5170,11 +5170,6 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" -entities@^4.2.0, entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"