diff --git a/lerna.json b/lerna.json
index 125fde7d69c..2eedc5c75c2 100644
--- a/lerna.json
+++ b/lerna.json
@@ -7,7 +7,8 @@
 		"packages/analytics",
 		"packages/storage",
 		"packages/aws-amplify",
-		"packages/adapter-nextjs"
+		"packages/adapter-nextjs",
+		"packages/rtn-web-browser"
 	],
 	"exact": true,
 	"version": "independent",
diff --git a/package.json b/package.json
index 3404a7d56da..cb830b605b7 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,8 @@
 			"packages/analytics",
 			"packages/storage",
 			"packages/aws-amplify",
-			"packages/adapter-nextjs"
+			"packages/adapter-nextjs",
+			"packages/rtn-web-browser"
 		],
 		"nohoist": [
 			"**/@types/react-native",
diff --git a/packages/rtn-web-browser/AmplifyRTNWebBrowser.podspec b/packages/rtn-web-browser/AmplifyRTNWebBrowser.podspec
new file mode 100644
index 00000000000..ba97ac5f91a
--- /dev/null
+++ b/packages/rtn-web-browser/AmplifyRTNWebBrowser.podspec
@@ -0,0 +1,35 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
+
+Pod::Spec.new do |s|
+  s.name         = "AmplifyRTNWebBrowser"
+  s.version      = package["version"]
+  s.summary      = package["description"]
+  s.homepage     = package["homepage"]
+  s.license      = package["license"]
+  s.authors      = package["author"]
+
+  s.platforms    = { :ios => "13.0" }
+  s.source       = { :git => "https://github.com/aws-amplify/amplify-js.git", :tag => "#{s.version}" }
+
+  s.source_files = "ios/**/*.{h,m,mm,swift}"
+
+  s.dependency "React-Core"
+
+  # Don't install the dependencies when we run `pod install` in the old architecture.
+  if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
+    s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
+    s.pod_target_xcconfig    = {
+        "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
+        "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
+        "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
+    }
+    s.dependency "React-Codegen"
+    s.dependency "RCT-Folly"
+    s.dependency "RCTRequired"
+    s.dependency "RCTTypeSafety"
+    s.dependency "ReactCommon/turbomodule/core"
+  end
+end
diff --git a/packages/rtn-web-browser/CHANGELOG.md b/packages/rtn-web-browser/CHANGELOG.md
new file mode 100644
index 00000000000..e4d87c4d45c
--- /dev/null
+++ b/packages/rtn-web-browser/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
diff --git a/packages/rtn-web-browser/android/build.gradle b/packages/rtn-web-browser/android/build.gradle
new file mode 100644
index 00000000000..9916e68d8a9
--- /dev/null
+++ b/packages/rtn-web-browser/android/build.gradle
@@ -0,0 +1,68 @@
+buildscript {
+    def kotlin_version = rootProject.ext.has('kotlinVersion')
+            ? rootProject.ext.get('kotlinVersion')
+            : project.properties['default_kotlinVersion']
+
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        if (project == rootProject) {
+            classpath 'com.android.tools.build:gradle:7.3.1'
+        }
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+def getExtOrDefault(prop) {
+    (rootProject.ext.has(prop) ? rootProject.ext.get(prop) : project.properties["default_$prop"]).toInteger()
+}
+
+android {
+    compileSdkVersion getExtOrDefault('compileSdkVersion')
+
+    defaultConfig {
+        minSdkVersion getExtOrDefault('minSdkVersion')
+        targetSdkVersion getExtOrDefault('targetSdkVersion')
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    testOptions {
+        unitTests {
+            includeAndroidResources = true
+        }
+    }
+}
+
+repositories {
+    // React Native installed via NPM location in application
+    maven { url "$rootDir/../node_modules/react-native/android" }
+    // React Native installed via NPM location in Amplify monorepo
+    maven { url "$rootDir/../../../node_modules/react-native/android" }
+    google()
+    mavenCentral()
+}
+
+dependencies {
+    //noinspection GradleDynamicVersion
+    implementation 'com.facebook.react:react-native:+'
+
+    // Import the browser library to support Custom Tabs
+    implementation 'androidx.browser:browser:1.5.0'
+
+    // Test dependencies
+    testImplementation 'junit:junit:4.13.2'
+    testImplementation 'io.mockk:mockk:1.13.4'
+    testImplementation 'org.slf4j:slf4j-nop:2.0.7'
+    testImplementation 'org.robolectric:robolectric:4.9'
+    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
+}
diff --git a/packages/rtn-web-browser/android/gradle.properties b/packages/rtn-web-browser/android/gradle.properties
new file mode 100644
index 00000000000..02b19a0a2ed
--- /dev/null
+++ b/packages/rtn-web-browser/android/gradle.properties
@@ -0,0 +1,23 @@
+## For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+#Mon Oct 31 14:53:15 PDT 2022
+android.nonTransitiveRClass=true
+kotlin.code.style=official
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding\=UTF-8
+android.useAndroidX=true
+android.enableJetifier=true
+
+default_kotlinVersion=1.7.20
+default_compileSdkVersion=32
+default_minSdkVersion=24
+default_targetSdkVersion=30
diff --git a/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.jar b/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000000..41d9927a4d4
Binary files /dev/null and b/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.properties b/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..41dfb87909a
--- /dev/null
+++ b/packages/rtn-web-browser/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/rtn-web-browser/android/gradlew b/packages/rtn-web-browser/android/gradlew
new file mode 100755
index 00000000000..1b6c787337f
--- /dev/null
+++ b/packages/rtn-web-browser/android/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/packages/rtn-web-browser/android/gradlew.bat b/packages/rtn-web-browser/android/gradlew.bat
new file mode 100644
index 00000000000..ac1b06f9382
--- /dev/null
+++ b/packages/rtn-web-browser/android/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/rtn-web-browser/android/src/main/AndroidManifest.xml b/packages/rtn-web-browser/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..9a24d1ecac8
--- /dev/null
+++ b/packages/rtn-web-browser/android/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.amazonaws.amplify.rtnwebbrowser">
+
+    <queries>
+        <intent>
+            <action android:name="android.support.customtabs.action.CustomTabsService" />
+        </intent>
+    </queries>
+</manifest>
diff --git a/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/CustomTabsHelper.kt b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/CustomTabsHelper.kt
new file mode 100644
index 00000000000..c47933f2df8
--- /dev/null
+++ b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/CustomTabsHelper.kt
@@ -0,0 +1,54 @@
+package com.amazonaws.amplify.rtnwebbrowser
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService
+
+private const val DUMMY_URL = "http://www.example.com"
+
+internal object CustomTabsHelper {
+    private var customTabsPackage: String? = null
+
+    fun getCustomTabsPackageName(context: Context): String? {
+        customTabsPackage?.let { return customTabsPackage }
+        val packageManager = context.packageManager
+        val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse(DUMMY_URL))
+        // Get default VIEW intent handler
+        val defaultViewHandlerPackage = packageManager.resolveActivity(
+            activityIntent,
+            PackageManager.MATCH_DEFAULT_ONLY
+        )?.activityInfo?.packageName ?: ""
+
+        // Get all apps that can handle VIEW intents
+        val resolvedActivityList =
+            packageManager.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL)
+
+        // Get all apps that can handle both VIEW intents and service calls
+        val packagesSupportingCustomTabs = ArrayList<String>()
+        resolvedActivityList.forEach { resolveInfo ->
+            val serviceIntent = Intent()
+                .setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION)
+                .setPackage(resolveInfo.activityInfo.packageName)
+            packageManager.resolveService(serviceIntent, PackageManager.MATCH_ALL)?.let {
+                packagesSupportingCustomTabs.add(it.serviceInfo.packageName)
+            }
+        }
+
+        customTabsPackage = if (packagesSupportingCustomTabs.isEmpty()) {
+            // If no packages support custom tabs, return null
+            null
+        } else if (defaultViewHandlerPackage.isNotEmpty() && packagesSupportingCustomTabs.contains(
+                defaultViewHandlerPackage
+            )
+        ) {
+            // Prefer the default browser if it supports Custom Tabs
+            defaultViewHandlerPackage
+        } else {
+            // Otherwise, pick the next favorite Custom Tabs provider
+            packagesSupportingCustomTabs[0]
+        }
+        return customTabsPackage
+    }
+}
diff --git a/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserModule.kt b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserModule.kt
new file mode 100644
index 00000000000..29ff64f346b
--- /dev/null
+++ b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserModule.kt
@@ -0,0 +1,73 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.amplify.rtnwebbrowser
+
+import android.content.Intent
+import android.net.Uri
+import android.util.Patterns
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsIntent
+import com.amazonaws.amplify.rtnwebbrowser.CustomTabsHelper.getCustomTabsPackageName
+import com.facebook.react.bridge.LifecycleEventListener
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import java.lang.Exception
+
+
+private val TAG = WebBrowserModule::class.java.simpleName
+
+class WebBrowserModule(
+    reactContext: ReactApplicationContext,
+) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
+
+    private var connection: WebBrowserServiceConnection? = null
+
+    init {
+        reactContext.addLifecycleEventListener(this)
+        getCustomTabsPackageName(reactApplicationContext)?.let {
+            connection = WebBrowserServiceConnection(reactApplicationContext)
+            CustomTabsClient.bindCustomTabsService(reactApplicationContext, it, connection!!)
+        }
+    }
+
+    @ReactMethod
+    fun openAuthSessionAsync(uriStr: String, promise: Promise) {
+        if (!Patterns.WEB_URL.matcher(uriStr).matches()) {
+            promise.reject(Throwable("Provided url is invalid"))
+            return
+        }
+        try {
+            getCustomTabsPackageName(reactApplicationContext)?.let {
+                val customTabsIntent = CustomTabsIntent.Builder(connection?.getSession()).build()
+                customTabsIntent.intent.setPackage(it).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                customTabsIntent.launchUrl(reactApplicationContext, Uri.parse(uriStr))
+            } ?: run {
+                promise.reject(Throwable("No eligible browser found on device"))
+            }
+        } catch (e: Exception) {
+            promise.reject(e)
+        }
+		promise.resolve(null)
+    }
+
+    override fun onHostResume() {
+        // noop - only overridden as this class implements LifecycleEventListener
+    }
+
+    override fun onHostPause() {
+        // noop - only overridden as this class implements LifecycleEventListener
+    }
+
+    override fun onHostDestroy() {
+        connection?.destroy()
+        connection = null
+    }
+
+
+    override fun getName() = "AmplifyRTNWebBrowser"
+
+    override fun getConstants(): MutableMap<String, Any> = hashMapOf()
+}
diff --git a/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserPackage.kt b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserPackage.kt
new file mode 100644
index 00000000000..88df29ed3eb
--- /dev/null
+++ b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserPackage.kt
@@ -0,0 +1,22 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.amplify.rtnwebbrowser
+
+import android.view.View
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ReactShadowNode
+import com.facebook.react.uimanager.ViewManager
+
+class WebBrowserPackage : ReactPackage {
+
+    override fun createViewManagers(
+        reactContext: ReactApplicationContext
+    ): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
+
+    override fun createNativeModules(
+        reactContext: ReactApplicationContext
+    ): MutableList<NativeModule> = listOf(WebBrowserModule(reactContext)).toMutableList()
+}
diff --git a/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserServiceConnection.kt b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserServiceConnection.kt
new file mode 100644
index 00000000000..8a638238d8f
--- /dev/null
+++ b/packages/rtn-web-browser/android/src/main/kotlin/com/amazonaws/amplify/rtnwebbrowser/WebBrowserServiceConnection.kt
@@ -0,0 +1,50 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.amplify.rtnwebbrowser
+
+import android.content.ComponentName
+import android.content.Context
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsServiceConnection
+import androidx.browser.customtabs.CustomTabsSession
+import com.amazonaws.amplify.rtnwebbrowser.CustomTabsHelper.getCustomTabsPackageName
+
+internal class WebBrowserServiceConnection(
+    private val context: Context
+) : CustomTabsServiceConnection() {
+    private var customTabsPackage: String? = getCustomTabsPackageName(context)
+    private var session: CustomTabsSession? = null
+    private var client: CustomTabsClient? = null
+
+    init {
+        session = client?.newSession(null)
+    }
+
+    fun destroy() {
+        if (customTabsPackage != null) {
+            context.unbindService(this)
+        }
+        customTabsPackage = null
+        client = null
+        session = null
+    }
+
+    fun getSession(): CustomTabsSession? {
+        return session
+    }
+
+    override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
+        if (name.packageName === customTabsPackage) {
+            client.warmup(0L)
+            session = client.newSession(null)
+            this.client = client
+        }
+    }
+
+    override fun onServiceDisconnected(name: ComponentName) {
+        if (name.packageName === customTabsPackage) {
+            destroy()
+        }
+    }
+}
diff --git a/packages/rtn-web-browser/build.js b/packages/rtn-web-browser/build.js
new file mode 100644
index 00000000000..5bdcce15dc5
--- /dev/null
+++ b/packages/rtn-web-browser/build.js
@@ -0,0 +1,7 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+'use strict';
+
+const build = require('../../scripts/build');
+
+build(process.argv[2], process.argv[3]);
diff --git a/packages/rtn-web-browser/index.js b/packages/rtn-web-browser/index.js
new file mode 100644
index 00000000000..669528c7e58
--- /dev/null
+++ b/packages/rtn-web-browser/index.js
@@ -0,0 +1,9 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+	module.exports = require('./dist/aws-amplify-rtn-web-browser.min.js');
+} else {
+	module.exports = require('./dist/aws-amplify-rtn-web-browser.js');
+}
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser-Bridging-Header.h b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser-Bridging-Header.h
new file mode 100644
index 00000000000..d2af4b10323
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser-Bridging-Header.h
@@ -0,0 +1,6 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#import <React/RCTBridgeModule.h>
+#import <React/RCTViewManager.h>
+#import <React/RCTRootView.h>
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.m b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.m
new file mode 100644
index 00000000000..35b4998f92c
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.m
@@ -0,0 +1,12 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#import <React/RCTBridgeModule.h>
+
+@interface RCT_EXTERN_MODULE(AmplifyRTNWebBrowser, NSObject)
+
+RCT_EXTERN_METHOD(openAuthSessionAsync:(NSString*)url
+                  resolve:(RCTPromiseResolveBlock)resolve
+                  reject:(RCTPromiseRejectBlock)reject)
+
+@end
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.swift b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.swift
new file mode 100644
index 00000000000..1a7b3d9d815
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.swift
@@ -0,0 +1,63 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import Foundation
+import AuthenticationServices
+
+private class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
+    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+        return ASPresentationAnchor()
+    }
+}
+
+@objc(AmplifyRTNWebBrowser)
+class AmplifyRTNWebBrowser: NSObject {
+    var webBrowserAuthSession: ASWebAuthenticationSession?
+    private let presentationContextProvider = PresentationContextProvider()
+    
+    private func isUrlValid(url: URL) -> Bool {
+        return url.scheme == "http" || url.scheme == "https"
+    }
+    
+    @objc
+    func openAuthSessionAsync(_ urlStr: String,
+                              resolve: @escaping RCTPromiseResolveBlock,
+                              reject: @escaping RCTPromiseRejectBlock) {
+        guard let url = URL(string: urlStr) else {
+            reject("ERROR", "provided url is invalid", nil)
+            return
+        }
+        
+        guard isUrlValid(url: url) else {
+            reject("ERROR", "provided url is invalid", nil)
+            return
+        }
+        
+        let authSession = ASWebAuthenticationSession(
+            url: url,
+            callbackURLScheme: nil,
+            completionHandler: { url, error in
+                if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
+                    reject("ERROR", "user canceled auth session", error)
+                    return
+                }
+                if error != nil {
+                    reject("ERROR", "error occurred starting auth session", error)
+                    return
+                }
+                resolve(url?.absoluteString)
+            })
+        webBrowserAuthSession = authSession
+        authSession.presentationContextProvider = presentationContextProvider
+        authSession.prefersEphemeralWebBrowserSession = true
+        
+        DispatchQueue.main.async {
+            authSession.start()
+        }
+    }
+    
+    @objc
+    static func requiresMainQueueSetup() -> Bool {
+        return true
+    }
+}
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.pbxproj b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.pbxproj
new file mode 100644
index 00000000000..bdce09ecb07
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.pbxproj
@@ -0,0 +1,170 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXFileReference section */
+		D31047282A81EA8200CD9A8D /* AmplifyRTNWebBrowser-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AmplifyRTNWebBrowser-Bridging-Header.h"; sourceTree = "<group>"; };
+		D31047292A81EA8200CD9A8D /* AmplifyRTNWebBrowser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AmplifyRTNWebBrowser.m; sourceTree = "<group>"; };
+		D310472D2A81EA8200CD9A8D /* AmplifyRTNWebBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmplifyRTNWebBrowser.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXGroup section */
+		134814211AA4EA7D00B7C361 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		58B511D21A9E6C8500147676 = {
+			isa = PBXGroup;
+			children = (
+				D31047282A81EA8200CD9A8D /* AmplifyRTNWebBrowser-Bridging-Header.h */,
+				D31047292A81EA8200CD9A8D /* AmplifyRTNWebBrowser.m */,
+				D310472D2A81EA8200CD9A8D /* AmplifyRTNWebBrowser.swift */,
+				134814211AA4EA7D00B7C361 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXProject section */
+		58B511D31A9E6C8500147676 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0920;
+				ORGANIZATIONNAME = Facebook;
+			};
+			buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "AmplifyRTNWebBrowser" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				English,
+				en,
+			);
+			mainGroup = 58B511D21A9E6C8500147676;
+			productRefGroup = 58B511D21A9E6C8500147676;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+			);
+		};
+/* End PBXProject section */
+
+/* Begin XCBuildConfiguration section */
+		58B511ED1A9E6C8500147676 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				"EXCLUDED_ARCHS[sdk=*]" = arm64;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+			};
+			name = Debug;
+		};
+		58B511EE1A9E6C8500147676 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = YES;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				"EXCLUDED_ARCHS[sdk=*]" = arm64;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "AmplifyRTNWebBrowser" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				58B511ED1A9E6C8500147676 /* Debug */,
+				58B511EE1A9E6C8500147676 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 58B511D31A9E6C8500147676 /* Project object */;
+}
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..94b2795e225
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+</Workspace>
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000000..18d981003d6
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/contents.xcworkspacedata b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..87165c39338
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:/Users/chrfang/Development/amplify-js/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000000..18d981003d6
--- /dev/null
+++ b/packages/rtn-web-browser/ios/AmplifyRTNWebBrowser.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/rtn-web-browser/package.json b/packages/rtn-web-browser/package.json
new file mode 100644
index 00000000000..d21ac899ee0
--- /dev/null
+++ b/packages/rtn-web-browser/package.json
@@ -0,0 +1,47 @@
+{
+	"name": "@aws-amplify/rtn-web-browser",
+	"version": "1.0.0",
+	"description": "React Native module for aws-amplify web browser",
+	"main": "./lib/index.js",
+	"module": "./lib-esm/index.js",
+	"typings": "./lib-esm/index.d.ts",
+	"sideEffects": false,
+	"publishConfig": {
+		"access": "public"
+	},
+	"scripts": {
+		"test": "tslint 'src/**/*.ts'",
+		"test:android": "./android/gradlew test -p ./android",
+		"build-with-test": "npm run clean && npm test && tsc && webpack",
+		"build:cjs": "node ./build es5 && webpack && webpack --config ./webpack.config.dev.js",
+		"build:esm": "node ./build es6",
+		"build:cjs:watch": "node ./build es5 --watch",
+		"build:esm:watch": "node ./build es6 --watch",
+		"build": "npm run clean && npm run build:esm && npm run build:cjs",
+		"clean": "rimraf lib-esm lib dist",
+		"format": "echo \"Not implemented\"",
+		"lint": "tslint 'src/**/*.ts' && npm run ts-coverage",
+		"ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 88.21"
+	},
+	"react-native": {
+		"./lib/index": "./lib-esm/index.js"
+	},
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/aws-amplify/amplify-js.git"
+	},
+	"author": "Amazon Web Services",
+	"license": "Apache-2.0",
+	"bugs": {
+		"url": "https://github.com/aws/aws-amplify/issues"
+	},
+	"homepage": "https://docs.amplify.aws/",
+	"files": [
+		"Amplify*.podspec",
+		"android",
+		"ios",
+		"lib",
+		"lib-esm",
+		"src"
+	]
+}
diff --git a/packages/rtn-web-browser/src/apis/openAuthSessionAsync.ts b/packages/rtn-web-browser/src/apis/openAuthSessionAsync.ts
new file mode 100644
index 00000000000..242ef8a7897
--- /dev/null
+++ b/packages/rtn-web-browser/src/apis/openAuthSessionAsync.ts
@@ -0,0 +1,71 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { AppState, Linking, NativeModules, Platform } from 'react-native';
+import { WebBrowserNativeModule } from '../types';
+
+const module: WebBrowserNativeModule = NativeModules.AmplifyRTNWebBrowser;
+
+let appStateListener;
+let redirectListener;
+
+export const openAuthSessionAsync = async (
+	url: string,
+	redirectSchemes: string[]
+) => {
+	// enforce HTTPS
+	const httpsUrl = url.replace('http://', 'https://');
+	if (Platform.OS === 'ios') {
+		return module.openAuthSessionAsync(httpsUrl);
+	}
+
+	if (Platform.OS === 'android') {
+		return openAuthSessionAndroid(httpsUrl, redirectSchemes);
+	}
+};
+
+const openAuthSessionAndroid = async (
+	url: string,
+	redirectSchemes: string[]
+) => {
+	try {
+		const [redirectUrl] = await Promise.all([
+			Promise.race([
+				// wait for app to redirect, resulting in a redirectUrl
+				getRedirectPromise(redirectSchemes),
+				// wait for app to return some other way, resulting in null
+				getAppStatePromise(),
+			]),
+			// open chrome tab
+			module.openAuthSessionAsync(url),
+		]);
+		return redirectUrl;
+	} finally {
+		appStateListener.remove();
+		redirectListener.remove();
+	}
+};
+
+const getAppStatePromise = (): Promise<null> =>
+	new Promise(resolve => {
+		appStateListener = AppState.addEventListener('change', nextAppState => {
+			// if current state is null, the change is from initialization
+			if (AppState.currentState === null) {
+				return;
+			}
+
+			if (nextAppState === 'active') {
+				appStateListener.remove();
+				resolve(null);
+			}
+		});
+	});
+
+const getRedirectPromise = (redirectSchemes: string[]): Promise<string> =>
+	new Promise(resolve => {
+		redirectListener = Linking.addEventListener('url', event => {
+			if (redirectSchemes.some(scheme => event.url.startsWith(scheme))) {
+				resolve(event.url);
+			}
+		});
+	});
diff --git a/packages/rtn-web-browser/src/index.ts b/packages/rtn-web-browser/src/index.ts
new file mode 100644
index 00000000000..0c4976d2fd7
--- /dev/null
+++ b/packages/rtn-web-browser/src/index.ts
@@ -0,0 +1,15 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { NativeModules } from 'react-native';
+import { WebBrowserNativeModule } from './types';
+import { openAuthSessionAsync } from './apis/openAuthSessionAsync';
+
+const module: WebBrowserNativeModule = NativeModules.AmplifyRTNWebBrowser;
+
+const mergedModule = {
+	...module,
+	openAuthSessionAsync,
+};
+
+export { mergedModule as AmplifyRTNWebBrowser };
diff --git a/packages/rtn-web-browser/src/types.ts b/packages/rtn-web-browser/src/types.ts
new file mode 100644
index 00000000000..5bfeb687488
--- /dev/null
+++ b/packages/rtn-web-browser/src/types.ts
@@ -0,0 +1,6 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export type WebBrowserNativeModule = {
+	openAuthSessionAsync: (url: string) => Promise<string | null>;
+};
diff --git a/packages/rtn-web-browser/tsconfig.build.json b/packages/rtn-web-browser/tsconfig.build.json
new file mode 100644
index 00000000000..af6adca185d
--- /dev/null
+++ b/packages/rtn-web-browser/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+	"extends": "../tsconfig.base.json",
+	"compilerOptions": {},
+	"include": ["lib*/**/*.ts", "src"]
+}
diff --git a/packages/rtn-web-browser/tsconfig.json b/packages/rtn-web-browser/tsconfig.json
new file mode 100755
index 00000000000..4b26b84eb1d
--- /dev/null
+++ b/packages/rtn-web-browser/tsconfig.json
@@ -0,0 +1,21 @@
+//WARNING: If you are manually specifying files to compile then the tsconfig.json is completely ignored, you must use command line flags
+{
+	"compilerOptions": {
+		"allowSyntheticDefaultImports": true,
+		"outDir": "./lib/",
+		"target": "es5",
+		"noImplicitAny": false,
+		"lib": ["dom", "es2019", "esnext.asynciterable"],
+		"sourceMap": true,
+		"module": "commonjs",
+		"moduleResolution": "node",
+		"allowJs": false,
+		"declaration": true,
+		"typeRoots": ["./node_modules/@types", "../../node_modules/@types"],
+		"types": ["node"],
+		"esModuleInterop": true,
+		"resolveJsonModule": true
+	},
+	"include": ["src/**/*"],
+	"exclude": ["src/setupTests.ts"]
+}
diff --git a/packages/rtn-web-browser/tslint.json b/packages/rtn-web-browser/tslint.json
new file mode 100644
index 00000000000..8eafab1d2b4
--- /dev/null
+++ b/packages/rtn-web-browser/tslint.json
@@ -0,0 +1,50 @@
+{
+	"defaultSeverity": "error",
+	"plugins": ["prettier"],
+	"extends": [],
+	"jsRules": {},
+	"rules": {
+		"prefer-const": true,
+		"max-line-length": [true, 120],
+		"no-empty-interface": true,
+		"no-var-keyword": true,
+		"object-literal-shorthand": true,
+		"no-eval": true,
+		"space-before-function-paren": [
+			true,
+			{
+				"anonymous": "never",
+				"named": "never"
+			}
+		],
+		"no-parameter-reassignment": true,
+		"align": [true, "parameters"],
+		"no-duplicate-imports": true,
+		"one-variable-per-declaration": [false, "ignore-for-loop"],
+		"triple-equals": [true, "allow-null-check"],
+		"comment-format": [true, "check-space"],
+		"indent": [false],
+		"whitespace": [
+			false,
+			"check-branch",
+			"check-decl",
+			"check-operator",
+			"check-preblock"
+		],
+		"eofline": true,
+		"variable-name": [
+			true,
+			"check-format",
+			"allow-pascal-case",
+			"allow-snake-case",
+			"allow-leading-underscore"
+		],
+		"semicolon": [
+			true,
+			"always",
+			"ignore-interfaces",
+			"ignore-bound-class-methods"
+		]
+	},
+	"rulesDirectory": []
+}
diff --git a/packages/rtn-web-browser/webpack.config.dev.js b/packages/rtn-web-browser/webpack.config.dev.js
new file mode 100644
index 00000000000..ae1e99b276e
--- /dev/null
+++ b/packages/rtn-web-browser/webpack.config.dev.js
@@ -0,0 +1,6 @@
+var config = require('./webpack.config.js');
+
+var entry = {
+	'aws-amplify-rtn-web-browser': './lib-esm/index.js',
+};
+module.exports = Object.assign(config, { entry, mode: 'development' });
diff --git a/packages/rtn-web-browser/webpack.config.js b/packages/rtn-web-browser/webpack.config.js
new file mode 100644
index 00000000000..ef56bd8b2a1
--- /dev/null
+++ b/packages/rtn-web-browser/webpack.config.js
@@ -0,0 +1,39 @@
+module.exports = {
+	entry: {
+		'aws-amplify-rtn-web-browser.min': './lib-esm/index.js',
+	},
+	externals: ['react-native'],
+	output: {
+		filename: '[name].js',
+		path: __dirname + '/dist',
+		library: 'aws_amplify_rtn_web_browser',
+		libraryTarget: 'umd',
+		umdNamedDefine: true,
+		globalObject: 'this',
+		devtoolModuleFilenameTemplate: require('../aws-amplify/webpack-utils')
+			.devtoolModuleFilenameTemplate,
+	},
+	// Enable sourcemaps for debugging webpack's output.
+	devtool: 'source-map',
+	resolve: {
+		extensions: ['.js', '.json'],
+	},
+	mode: 'production',
+	module: {
+		rules: [
+			{
+				test: /\.js?$/,
+				exclude: /node_modules/,
+				use: [
+					'babel-loader',
+					{
+						loader: 'babel-loader',
+						options: {
+							presets: ['@babel/preset-env'],
+						},
+					},
+				],
+			},
+		],
+	},
+};