├── .watchmanconfig
├── example
├── .watchmanconfig
├── jest.config.js
├── .bundle
│ └── config
├── .eslintrc.js
├── app.json
├── .prettierrc.js
├── android
│ ├── app
│ │ ├── debug.keystore
│ │ ├── src
│ │ │ └── main
│ │ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ └── drawable
│ │ │ │ │ └── rn_edit_text_material.xml
│ │ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── inappbrowsernitroexample
│ │ │ │ │ ├── MainApplication.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── AndroidManifest.xml
│ │ ├── proguard-rules.pro
│ │ └── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradlew.bat
│ └── gradlew
├── ios
│ ├── InAppBrowserNitroExample
│ │ ├── Images.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── PrivacyInfo.xcprivacy
│ │ ├── AppDelegate.swift
│ │ ├── Info.plist
│ │ └── LaunchScreen.storyboard
│ ├── InappbrowserNitroExample.xcworkspace
│ │ └── contents.xcworkspacedata
│ ├── .xcode.env
│ ├── Podfile
│ └── InAppBrowserNitroExample.xcodeproj
│ │ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── InAppBrowserNitroExample.xcscheme
│ │ └── project.pbxproj
├── index.js
├── tsconfig.json
├── react-native.config.js
├── metro.config.js
├── babel.config.js
├── Gemfile
├── package.json
├── .gitignore
├── Gemfile.lock
├── README.md
└── App.tsx
├── ios
├── Bridge.h
├── String+Nitro.swift
├── UIApplication+TopMost.swift
├── HybridInappbrowserNitro.swift
├── UIColor+DynamicColor.swift
├── AuthSessionManager.swift
└── SafariPresenter.swift
├── app.gif
├── .yarnrc.yml
├── babel.config.js
├── android
├── src
│ └── main
│ │ ├── cpp
│ │ └── cpp-adapter.cpp
│ │ ├── java
│ │ └── com
│ │ │ └── inappbrowsernitro
│ │ │ ├── InappbrowserNitroPackage.kt
│ │ │ ├── browser
│ │ │ ├── CustomTabsPackageHelper.kt
│ │ │ ├── BrowserFallback.kt
│ │ │ ├── DynamicColorResolver.kt
│ │ │ └── CustomTabsIntentFactory.kt
│ │ │ └── HybridInappbrowserNitro.kt
│ │ └── AndroidManifest.xml
├── gradle.properties
├── CMakeLists.txt
├── fix-prefab.gradle
└── build.gradle
├── nitro.json
├── post-script.js
├── src
├── utils
│ ├── url.ts
│ └── options.ts
├── index.ts
├── core
│ └── InAppBrowser.ts
├── hooks
│ └── useInAppBrowser.ts
└── specs
│ └── inappbrowser-nitro.nitro.ts
├── scripts
└── remove-source-maps.cjs
├── InAppBrowserNitro.podspec
├── tsconfig.json
├── .gitignore
├── LICENSE
├── .github
├── dependabot.yml
└── workflows
│ ├── release.yml
│ ├── android-build.yml
│ └── ios-build.yml
├── release.config.cjs
├── package.json
└── README.md
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ios/Bridge.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
--------------------------------------------------------------------------------
/example/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | };
4 |
--------------------------------------------------------------------------------
/app.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/app.gif
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-4.11.0.cjs
4 |
--------------------------------------------------------------------------------
/example/.bundle/config:
--------------------------------------------------------------------------------
1 | BUNDLE_PATH: "vendor/bundle"
2 | BUNDLE_FORCE_RUBY_PLATFORM: 1
3 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | }
4 |
--------------------------------------------------------------------------------
/example/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native',
4 | };
5 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "InappbrowserNitroExample",
3 | "displayName": "InappbrowserNitroExample"
4 | }
5 |
--------------------------------------------------------------------------------
/example/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | };
6 |
--------------------------------------------------------------------------------
/example/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/debug.keystore
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | InappbrowserNitroExample
3 |
4 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-native-inappbrowser-nitro/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/cpp/cpp-adapter.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "InappbrowserNitroOnLoad.hpp"
3 |
4 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5 | return margelo::nitro::inappbrowsernitro::initialize(vm);
6 | }
7 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | InappbrowserNitro_kotlinVersion=2.1.20
2 | InappbrowserNitro_minSdkVersion=23
3 | InappbrowserNitro_targetSdkVersion=35
4 | InappbrowserNitro_compileSdkVersion=34
5 | InappbrowserNitro_ndkVersion=27.1.12297006
6 |
--------------------------------------------------------------------------------
/ios/String+Nitro.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension String {
4 | var nitroTrimmedNonEmpty: String? {
5 | let result = trimmingCharacters(in: .whitespacesAndNewlines)
6 | return result.isEmpty ? nil : result
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import { AppRegistry } from 'react-native';
6 | import App from './App';
7 | import { name as appName } from './app.json';
8 |
9 | AppRegistry.registerComponent(appName, () => App);
10 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@react-native/typescript-config",
3 | "include": ["**/*.ts", "**/*.tsx"],
4 | "exclude": ["**/node_modules", "**/Pods"],
5 | "compilerOptions": {
6 | "strict": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "react-native-inappbrowser-nitro": ["../src"]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/example/ios/InappbrowserNitroExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement { includeBuild("../../node_modules/@react-native/gradle-plugin") }
2 | plugins { id("com.facebook.react.settings") }
3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
4 | rootProject.name = 'InappbrowserNitroExample'
5 | include ':app'
6 | includeBuild('../../node_modules/@react-native/gradle-plugin')
7 |
--------------------------------------------------------------------------------
/example/react-native.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const pkg = require('../package.json')
3 |
4 | /**
5 | * @type {import('@react-native-community/cli-types').Config}
6 | */
7 | module.exports = {
8 | project: {
9 | ios: {
10 | automaticPodsInstallation: true,
11 | },
12 | },
13 | dependencies: {
14 | [pkg.name]: {
15 | root: path.join(__dirname, '..'),
16 | },
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
2 | const path = require('path');
3 | const root = path.resolve(__dirname, '..');
4 |
5 | /**
6 | * Metro configuration
7 | * https://facebook.github.io/metro/docs/configuration
8 | *
9 | * @type {import('metro-config').MetroConfig}
10 | */
11 | const config = {
12 | watchFolders: [root],
13 | };
14 |
15 | module.exports = mergeConfig(getDefaultConfig(__dirname), config);
--------------------------------------------------------------------------------
/example/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pak = require('../package.json');
3 |
4 | module.exports = api => {
5 | api.cache(true);
6 | return {
7 | presets: ['module:@react-native/babel-preset'],
8 | plugins: [
9 | [
10 | 'module-resolver',
11 | {
12 | extensions: ['.js', '.ts', '.json', '.jsx', '.tsx'],
13 | alias: {
14 | [pak.name]: path.join(__dirname, '../', pak.source),
15 | },
16 | },
17 | ],
18 | ],
19 | };
20 | };
--------------------------------------------------------------------------------
/example/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/example/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures.
7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
9 | gem 'xcodeproj', '< 1.26.0'
10 | gem 'concurrent-ruby', '< 1.3.4'
11 |
12 | # Ruby 3.4.0 has removed some libraries from the standard library.
13 | gem 'bigdecimal'
14 | gem 'logger'
15 | gem 'benchmark'
16 | gem 'mutex_m'
17 |
--------------------------------------------------------------------------------
/nitro.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://nitro.margelo.com/nitro.schema.json",
3 | "cxxNamespace": [
4 | "inappbrowsernitro"
5 | ],
6 | "ios": {
7 | "iosModuleName": "InappbrowserNitro"
8 | },
9 | "android": {
10 | "androidNamespace": [
11 | "inappbrowsernitro"
12 | ],
13 | "androidCxxLibName": "InappbrowserNitro"
14 | },
15 | "autolinking": {
16 | "InappbrowserNitro": {
17 | "swift": "HybridInappbrowserNitro",
18 | "kotlin": "HybridInappbrowserNitro"
19 | }
20 | },
21 | "ignorePaths": [
22 | "**/node_modules"
23 | ]
24 | }
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "36.0.0"
4 | minSdkVersion = 24
5 | compileSdkVersion = 36
6 | targetSdkVersion = 36
7 | ndkVersion = "27.1.12297006"
8 | kotlinVersion = "2.1.20"
9 | }
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle")
16 | classpath("com.facebook.react:react-native-gradle-plugin")
17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18 | }
19 | }
20 |
21 | apply plugin: "com.facebook.react.rootproject"
22 |
--------------------------------------------------------------------------------
/ios/UIApplication+TopMost.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIApplication {
4 | var nitroTopMostViewController: UIViewController? {
5 | guard Thread.isMainThread else {
6 | return DispatchQueue.main.sync {
7 | self.nitroTopMostViewController
8 | }
9 | }
10 |
11 | let windowScene = connectedScenes
12 | .compactMap { $0 as? UIWindowScene }
13 | .flatMap { $0.windows }
14 | .first { $0.isKeyWindow }
15 |
16 | guard let root = windowScene?.rootViewController else {
17 | return nil
18 | }
19 |
20 | var candidate: UIViewController? = root
21 | while let presented = candidate?.presentedViewController {
22 | candidate = presented
23 | }
24 |
25 | return candidate
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/InappbrowserNitroPackage.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro;
2 |
3 | import com.facebook.react.bridge.NativeModule;
4 | import com.facebook.react.bridge.ReactApplicationContext;
5 | import com.facebook.react.module.model.ReactModuleInfoProvider;
6 | import com.facebook.react.TurboReactPackage;
7 | import com.margelo.nitro.inappbrowsernitro.InappbrowserNitroOnLoad;
8 |
9 |
10 | public class InappbrowserNitroPackage : TurboReactPackage() {
11 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
12 |
13 | override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
14 |
15 | companion object {
16 | init {
17 | InappbrowserNitroOnLoad.initializeNative();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/post-script.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file This script is auto-generated by create-nitro-module and should not be edited.
3 | *
4 | * @description This script applies a workaround for Android by modifying the 'OnLoad.cpp' file.
5 | * It reads the file content and removes the 'margelo/nitro/' string from it. This enables support for custom package names.
6 | *
7 | * @module create-nitro-module
8 | */
9 | const path = require('node:path')
10 | const { writeFile, readFile } = require('node:fs/promises')
11 |
12 | const androidWorkaround = async () => {
13 | const androidOnLoadFile = path.join(
14 | process.cwd(),
15 | 'nitrogen/generated/android',
16 | 'InappbrowserNitroOnLoad.cpp'
17 | )
18 |
19 | const str = await readFile(androidOnLoadFile, { encoding: 'utf8' })
20 |
21 | await writeFile(androidOnLoadFile, str.replace(/margelo\/nitro\//g, ''))
22 | }
23 |
24 | androidWorkaround()
--------------------------------------------------------------------------------
/android/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | project(InappbrowserNitro)
2 | cmake_minimum_required(VERSION 3.9.0)
3 |
4 | set (PACKAGE_NAME InappbrowserNitro)
5 | set (CMAKE_VERBOSE_MAKEFILE ON)
6 | set (CMAKE_CXX_STANDARD 20)
7 |
8 | # Enable Raw Props parsing in react-native (for Nitro Views)
9 | add_compile_options(-DRN_SERIALIZABLE_STATE=1)
10 |
11 | # Define C++ library and add all sources
12 | add_library(${PACKAGE_NAME} SHARED
13 | src/main/cpp/cpp-adapter.cpp
14 | )
15 |
16 | # Add Nitrogen specs :)
17 | include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/InappbrowserNitro+autolinking.cmake)
18 |
19 | # Set up local includes
20 | include_directories(
21 | "src/main/cpp"
22 | "../cpp"
23 | )
24 |
25 | find_library(LOG_LIB log)
26 |
27 | # Link all libraries together
28 | target_link_libraries(
29 | ${PACKAGE_NAME}
30 | ${LOG_LIB}
31 | android # <-- Android core
32 | )
33 |
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/browser/CustomTabsPackageHelper.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro.browser
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.browser.customtabs.CustomTabsClient
6 |
7 | internal object CustomTabsPackageHelper {
8 | fun resolvePackage(context: Context, preferred: String?): String? {
9 | if (!preferred.isNullOrBlank()) {
10 | return preferred
11 | }
12 |
13 | val chromePackage = "com.android.chrome"
14 | if (isPackageInstalled(context, chromePackage)) {
15 | return chromePackage
16 | }
17 |
18 | return CustomTabsClient.getPackageName(context, null)
19 | }
20 |
21 | private fun isPackageInstalled(context: Context, packageName: String): Boolean {
22 | return try {
23 | context.packageManager.getPackageInfo(packageName, 0)
24 | true
25 | } catch (e: PackageManager.NameNotFoundException) {
26 | false
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/inappbrowsernitroexample/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitroexample
2 |
3 | import android.app.Application
4 | import com.facebook.react.PackageList
5 | import com.facebook.react.ReactApplication
6 | import com.facebook.react.ReactHost
7 | import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
8 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
9 |
10 | class MainApplication : Application(), ReactApplication {
11 |
12 | override val reactHost: ReactHost by lazy {
13 | getDefaultReactHost(
14 | context = applicationContext,
15 | packageList =
16 | PackageList(this).packages.apply {
17 | // Packages that cannot be autolinked yet can be added manually here, for example:
18 | // add(MyReactNativePackage())
19 | },
20 | )
21 | }
22 |
23 | override fun onCreate() {
24 | super.onCreate()
25 | loadReactNative(this)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Validate and sanitize a URL string before passing it to the native layer.
3 | * Throws if the URL is empty, missing a scheme, or uses an unsafe scheme.
4 | */
5 | export const normalizeUrl = (candidate: string): string => {
6 | const trimmed = candidate?.trim()
7 |
8 | if (!trimmed) {
9 | throw new Error('URL must be a non-empty string.')
10 | }
11 |
12 | const schemeMatch = trimmed.match(/^([a-z][a-z0-9+.-]*):/i)
13 | if (!schemeMatch) {
14 | throw new Error('URL must include a valid URI scheme (e.g. https://).')
15 | }
16 |
17 | const scheme = schemeMatch[1]?.toLowerCase()
18 |
19 | if (!scheme) {
20 | throw new Error('URL scheme could not be determined.')
21 | }
22 |
23 | const deniedSchemes = new Set(['javascript', 'data', 'vbscript'])
24 |
25 | if (deniedSchemes.has(scheme)) {
26 | throw new Error(`The URI scheme "${scheme}" is not allowed for security reasons.`)
27 | }
28 |
29 | return trimmed
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/remove-source-maps.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const path = require('path')
4 | const { promises: fs } = require('fs')
5 |
6 | async function removeMaps(directory) {
7 | const entries = await fs.readdir(directory, { withFileTypes: true })
8 |
9 | await Promise.all(
10 | entries.map(async (entry) => {
11 | const entryPath = path.join(directory, entry.name)
12 |
13 | if (entry.isDirectory()) {
14 | await removeMaps(entryPath)
15 | return
16 | }
17 |
18 | if (entry.isFile() && entry.name.endsWith('.map')) {
19 | await fs.unlink(entryPath)
20 | }
21 | })
22 | )
23 | }
24 |
25 | async function main() {
26 | const libPath = path.join(__dirname, '..', 'lib')
27 |
28 | try {
29 | await fs.access(libPath)
30 | } catch {
31 | return
32 | }
33 |
34 | await removeMaps(libPath)
35 | }
36 |
37 | main().catch((error) => {
38 | console.error('Failed to remove source maps from lib directory:', error)
39 | process.exitCode = 1
40 | })
41 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/inappbrowsernitroexample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitroexample
2 |
3 | import com.facebook.react.ReactActivity
4 | import com.facebook.react.ReactActivityDelegate
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate
7 |
8 | class MainActivity : ReactActivity() {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | override fun getMainComponentName(): String = "InappbrowserNitroExample"
15 |
16 | /**
17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
19 | */
20 | override fun createReactActivityDelegate(): ReactActivityDelegate =
21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22 | }
23 |
--------------------------------------------------------------------------------
/InAppBrowserNitro.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = "InappbrowserNitro"
7 | s.version = package["version"]
8 | s.summary = package["description"]
9 | s.homepage = package["homepage"]
10 | s.license = package["license"]
11 | s.authors = package["author"]
12 |
13 | s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
14 | s.source = { :git => "https://github.com/mCodex/react-native-inappbrowser-nitro.git", :tag => "#{s.version}" }
15 |
16 | s.source_files = [
17 | # Implementation (Swift)
18 | "ios/**/*.{swift}",
19 | # Autolinking/Registration (Objective-C++)
20 | "ios/**/*.{m,mm}",
21 | # Implementation (C++ objects)
22 | "cpp/**/*.{hpp,cpp}",
23 | ]
24 |
25 | load 'nitrogen/generated/ios/InappbrowserNitro+autolinking.rb'
26 | add_nitrogen_files(s)
27 |
28 | s.dependency 'React-jsi'
29 | s.dependency 'React-callinvoker'
30 | install_modules_dependencies(s)
31 | end
32 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | linkage = ENV['USE_FRAMEWORKS']
12 | if linkage != nil
13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
14 | use_frameworks! :linkage => linkage.to_sym
15 | end
16 |
17 | target 'InappbrowserNitroExample' do
18 | config = use_native_modules!
19 |
20 | use_react_native!(
21 | :path => config[:reactNativePath],
22 | # An absolute path to your application root.
23 | :app_path => "#{Pod::Config.instance.installation_root}/.."
24 | )
25 |
26 | post_install do |installer|
27 | react_native_post_install(
28 | installer,
29 | config[:reactNativePath],
30 | :mac_catalyst_enabled => false,
31 | # :ccache_enabled => true
32 | )
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "allowUnreachableCode": false,
5 | "allowUnusedLabels": false,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "jsx": "react",
9 | "lib": ["esnext"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "noEmit": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitReturns": true,
15 | "noImplicitUseStrict": false,
16 | "noStrictGenericChecks": false,
17 | "noUncheckedIndexedAccess": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "resolveJsonModule": true,
21 | "skipLibCheck": true,
22 | "strict": true,
23 | "target": "esnext",
24 | "verbatimModuleSyntax": true
25 | },
26 | "exclude": [
27 | "**/node_modules",
28 | "**/lib",
29 | "**/.eslintrc.js",
30 | "**/.prettierrc.js",
31 | "**/jest.config.js",
32 | "**/babel.config.js",
33 | "**/metro.config.js",
34 | "**/tsconfig.json"
35 | ],
36 | "include": ["src/**/*", "nitrogen/**/*.json"]
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | # Xcode
13 | #
14 | build/
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | *.xccheckout
25 | *.moved-aside
26 | DerivedData
27 | *.hmap
28 | *.ipa
29 | *.xcuserstate
30 | project.xcworkspace
31 |
32 | # Android/IJ
33 | #
34 | .classpath
35 | .cxx
36 | .gradle
37 | .idea
38 | .project
39 | .settings
40 | local.properties
41 | android.iml
42 |
43 | # Cocoapods
44 | #
45 | example/ios/Pods
46 |
47 | # Ruby
48 | example/vendor/
49 |
50 | # node.js
51 | #
52 | node_modules/
53 | npm-debug.log
54 | yarn-debug.log
55 | yarn-error.log
56 |
57 | # BUCK
58 | buck-out/
59 | \.buckd/
60 | android/app/libs
61 | android/keystores/debug.keystore
62 |
63 | # Yarn
64 | .yarn/*
65 | !.yarn/patches
66 | !.yarn/plugins
67 | !.yarn/releases
68 | !.yarn/sdks
69 | !.yarn/versions
70 | .kotlin
71 |
72 | # Expo
73 | .expo/
74 |
75 | # generated by bob
76 | lib/
77 | tsconfig.tsbuildinfo
78 |
79 | nitrogen/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Mateus Andrade
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { InAppBrowser } from './core/InAppBrowser'
2 | import { useInAppBrowser } from './hooks/useInAppBrowser'
3 |
4 | import type {
5 | InAppBrowserAuthResult,
6 | InAppBrowserOptions,
7 | InAppBrowserResult,
8 | InAppBrowserAndroidOptions,
9 | InAppBrowserIOSOptions,
10 | BrowserAnimations,
11 | DynamicColor,
12 | } from './specs/inappbrowser-nitro.nitro'
13 | import {
14 | BrowserColorScheme,
15 | BrowserResultType,
16 | BrowserShareState,
17 | DismissButtonStyle,
18 | ModalPresentationStyle,
19 | ModalTransitionStyle,
20 | StatusBarStyle,
21 | UserInterfaceStyle,
22 | } from './specs/inappbrowser-nitro.nitro'
23 |
24 | InAppBrowser.useInAppBrowser = useInAppBrowser
25 |
26 | export { InAppBrowser, useInAppBrowser }
27 | export {
28 | BrowserColorScheme,
29 | BrowserResultType,
30 | BrowserShareState,
31 | DismissButtonStyle,
32 | ModalPresentationStyle,
33 | ModalTransitionStyle,
34 | StatusBarStyle,
35 | UserInterfaceStyle,
36 | }
37 | export type {
38 | InAppBrowserAuthResult,
39 | InAppBrowserOptions,
40 | InAppBrowserResult,
41 | InAppBrowserAndroidOptions,
42 | InAppBrowserIOSOptions,
43 | BrowserAnimations,
44 | DynamicColor,
45 | }
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | enable-beta-ecosystems: true
3 | updates:
4 | - package-ecosystem: 'github-actions'
5 | directory: '/'
6 | schedule:
7 | interval: 'daily'
8 | labels:
9 | - 'dependencies'
10 |
11 | - package-ecosystem: 'gradle'
12 | directories:
13 | - '/android/'
14 | - '/example/android/'
15 | schedule:
16 | interval: 'daily'
17 | labels:
18 | - 'nitro-core'
19 | - 'nitrogen'
20 | - 'dependencies'
21 | - 'kotlin'
22 |
23 | - package-ecosystem: 'bundler'
24 | directory: '/example/'
25 | schedule:
26 | interval: 'daily'
27 | labels:
28 | - 'dependencies'
29 | - 'ruby'
30 |
31 | - package-ecosystem: 'npm'
32 | directories:
33 | - '/example/'
34 | - '/'
35 | schedule:
36 | interval: 'daily'
37 | labels:
38 | - 'nitro-core'
39 | - 'dependencies'
40 | - 'typescript'
41 | - 'nitrogen'
42 |
43 | groups:
44 | react-native-cli:
45 | patterns:
46 | - '@react-native-community/cli*'
47 | babel:
48 | patterns:
49 | - '@babel/*'
50 | react-native:
51 | patterns:
52 | - '@react-native/*'
53 | nitro:
54 | patterns:
55 | - 'nitrogen'
56 | - 'react-native-nitro-modules'
57 |
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/browser/BrowserFallback.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro.browser
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.net.Uri
8 | import androidx.core.net.toUri
9 |
10 | internal class BrowserFallback(private val context: Context) {
11 | fun openSystemBrowser(url: String): Boolean {
12 | val uri = url.toUri()
13 | val fallbackIntent = Intent(Intent.ACTION_VIEW, uri)
14 | return launchSafely(fallbackIntent)
15 | }
16 |
17 | fun openChooser(url: String): Boolean {
18 | val uri = url.toUri()
19 | val intent = Intent(Intent.ACTION_VIEW, uri)
20 | val chooser = Intent.createChooser(intent, "Choose browser")
21 | return launchSafely(chooser)
22 | }
23 |
24 | fun redirectToStore(): Boolean {
25 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=browser"))
26 | return launchSafely(intent)
27 | }
28 |
29 | private fun launchSafely(intent: Intent): Boolean {
30 | if (context !is Activity) {
31 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
32 | }
33 |
34 | return try {
35 | context.startActivity(intent)
36 | true
37 | } catch (_: ActivityNotFoundException) {
38 | false
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-inappbrowser-nitro-example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios --simulator='iPhone 16'",
8 | "lint": "eslint .",
9 | "start": "react-native start --reset-cache",
10 | "test": "jest",
11 | "pod": "bundle install && bundle exec pod install --project-directory=ios"
12 | },
13 | "dependencies": {
14 | "@react-native/new-app-screen": "0.83.0",
15 | "react": "19.2.3",
16 | "react-native": "0.83.0",
17 | "react-native-nitro-modules": "0.31.10",
18 | "react-native-safe-area-context": "^5.6.2"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.28.5",
22 | "@babel/preset-env": "^7.28.5",
23 | "@babel/runtime": "^7.28.4",
24 | "@react-native-community/cli": "20.0.2",
25 | "@react-native-community/cli-platform-android": "20.0.2",
26 | "@react-native-community/cli-platform-ios": "20.0.2",
27 | "@react-native/babel-preset": "0.83.0",
28 | "@react-native/eslint-config": "0.83.0",
29 | "@react-native/metro-config": "0.83.0",
30 | "@react-native/typescript-config": "0.83.0",
31 | "@types/jest": "^30.0.0",
32 | "babel-plugin-module-resolver": "^5.0.2",
33 | "eslint": "^9.39.2",
34 | "prettier": "^3.7.4"
35 | },
36 | "engines": {
37 | "node": ">=20"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | **/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 | .kotlin/
37 |
38 | # node.js
39 | #
40 | node_modules/
41 | npm-debug.log
42 | yarn-error.log
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | **/fastlane/report.xml
52 | **/fastlane/Preview.html
53 | **/fastlane/screenshots
54 | **/fastlane/test_output
55 |
56 | # Bundle artifact
57 | *.jsbundle
58 |
59 | # Ruby / CocoaPods
60 | **/Pods/
61 | /vendor/bundle/
62 |
63 | # Temporary files created by Metro to check the health of the file watcher
64 | .metro-health-check*
65 |
66 | # testing
67 | /coverage
68 |
69 | # Yarn
70 | .yarn/*
71 | !.yarn/patches
72 | !.yarn/plugins
73 | !.yarn/releases
74 | !.yarn/sdks
75 | !.yarn/versions
76 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import React
3 | import React_RCTAppDelegate
4 | import ReactAppDependencyProvider
5 |
6 | @main
7 | class AppDelegate: UIResponder, UIApplicationDelegate {
8 | var window: UIWindow?
9 |
10 | var reactNativeDelegate: ReactNativeDelegate?
11 | var reactNativeFactory: RCTReactNativeFactory?
12 |
13 | func application(
14 | _ application: UIApplication,
15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
16 | ) -> Bool {
17 | let delegate = ReactNativeDelegate()
18 | let factory = RCTReactNativeFactory(delegate: delegate)
19 | delegate.dependencyProvider = RCTAppDependencyProvider()
20 |
21 | reactNativeDelegate = delegate
22 | reactNativeFactory = factory
23 |
24 | window = UIWindow(frame: UIScreen.main.bounds)
25 |
26 | factory.startReactNative(
27 | withModuleName: "InappbrowserNitroExample",
28 | in: window,
29 | launchOptions: launchOptions
30 | )
31 |
32 | return true
33 | }
34 | }
35 |
36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
37 | override func sourceURL(for bridge: RCTBridge) -> URL? {
38 | self.bundleURL()
39 | }
40 |
41 | override func bundleURL() -> URL? {
42 | #if DEBUG
43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
44 | #else
45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle")
46 | #endif
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ios/HybridInappbrowserNitro.swift:
--------------------------------------------------------------------------------
1 | import NitroModules
2 |
3 | final class HybridInappbrowserNitro: HybridInappbrowserNitroSpec {
4 | private let safariPresenter = SafariPresenter()
5 | private let authManager = AuthSessionManager()
6 |
7 | func isAvailable() throws -> Promise {
8 | Promise.resolved(withResult: true)
9 | }
10 |
11 | func open(url: String, options: InAppBrowserOptions?) throws -> Promise {
12 | Promise.async { [weak self] in
13 | guard let self else {
14 | return InAppBrowserResult(type: .dismiss, url: nil, message: "module released")
15 | }
16 |
17 | return await self.safariPresenter.present(urlString: url, options: options)
18 | }
19 | }
20 |
21 | func openAuth(url: String, redirectUrl: String, options: InAppBrowserOptions?) throws -> Promise {
22 | Promise.async { [weak self] in
23 | guard let self else {
24 | return InAppBrowserAuthResult(type: .dismiss, url: nil, message: "module released")
25 | }
26 |
27 | return await self.authManager.start(urlString: url, redirectUrl: redirectUrl, options: options)
28 | }
29 | }
30 |
31 | func close() throws -> Promise {
32 | Promise.async { [weak self] in
33 | guard let self else { return }
34 |
35 | await self.safariPresenter.dismiss()
36 | }
37 | }
38 |
39 | func closeAuth() throws -> Promise {
40 | Promise.async { [weak self] in
41 | guard let self else { return }
42 |
43 | await self.authManager.cancel()
44 | }
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | contents: read
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | release:
15 | name: Release
16 | runs-on: ubuntu-latest
17 | permissions:
18 | contents: write
19 | issues: write
20 | pull-requests: write
21 | id-token: write
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 | - name: Setup Bun.js
28 | uses: oven-sh/setup-bun@v2
29 | with:
30 | bun-version: latest
31 | - name: Cache bun dependencies
32 | id: bun-cache
33 | uses: actions/cache@v4
34 | with:
35 | path: ~/.bun/install/cache
36 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
37 | restore-keys: |
38 | ${{ runner.os }}-bun-
39 |
40 | - name: Install npm dependencies (bun)
41 | run: bun install
42 |
43 | - name: Build lib
44 | run: bun run build
45 |
46 | - name: Release
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
50 | NPM_CONFIG_PROVENANCE: true
51 | GIT_AUTHOR_NAME: ${{ github.actor }}
52 | GIT_AUTHOR_EMAIL: '${{ github.actor }}@users.noreply.github.com'
53 | GIT_COMMITTER_NAME: ${{ github.actor }}
54 | GIT_COMMITTER_EMAIL: '${{ github.actor }}@users.noreply.github.com'
55 | run: bun release
56 |
--------------------------------------------------------------------------------
/release.config.cjs:
--------------------------------------------------------------------------------
1 | const rules = [
2 | { type: 'feat', release: 'minor', title: '✨ Features' },
3 | { type: 'fix', release: 'patch', title: '🐛 Bug Fixes' },
4 | { type: 'perf', release: 'patch', title: '💨 Performance Improvements' },
5 | { type: 'refactor', release: 'patch', title: '🔄 Code Refactors' },
6 | { type: 'docs', release: 'patch', title: '📚 Documentation' },
7 | { type: 'chore', release: 'patch', title: '🛠️ Other changes' },
8 | ]
9 |
10 | const sortMap = Object.fromEntries(
11 | rules.map((rule, index) => [rule.title, index])
12 | )
13 |
14 | /**
15 | * @type {import('semantic-release').GlobalConfig}
16 | */
17 | module.exports = {
18 | branches: ['main', { name: 'next', prerelease: 'next' }],
19 | plugins: [
20 | [
21 | '@semantic-release/commit-analyzer',
22 | {
23 | preset: 'conventionalcommits',
24 | releaseRules: [
25 | { breaking: true, release: 'major' },
26 | { revert: true, release: 'patch' },
27 | ].concat(rules.map(({ type, release }) => ({ type, release }))),
28 | },
29 | ],
30 | [
31 | '@semantic-release/release-notes-generator',
32 | {
33 | preset: 'conventionalcommits',
34 | presetConfig: {
35 | types: rules.map(({ type, title }) => ({
36 | type,
37 | section: title,
38 | })),
39 | },
40 | writerOpts: {
41 | commitGroupsSort: (a, z) => sortMap[a.title] - sortMap[z.title],
42 | },
43 | },
44 | ],
45 | [
46 | '@semantic-release/changelog',
47 | {
48 | changelogFile: 'CHANGELOG.md',
49 | },
50 | ],
51 | '@semantic-release/npm',
52 | '@semantic-release/github',
53 | [
54 | '@semantic-release/git',
55 | {
56 | assets: ['package.json', 'CHANGELOG.md', 'example/package.json'],
57 | },
58 | ],
59 | ],
60 | }
61 |
--------------------------------------------------------------------------------
/ios/UIColor+DynamicColor.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIColor {
4 | static func from(dynamicColor: DynamicColor?) -> UIColor? {
5 | guard let dynamicColor else { return nil }
6 |
7 | let base = dynamicColor.base?.nitroColor
8 | let light = dynamicColor.light?.nitroColor
9 | let dark = dynamicColor.dark?.nitroColor
10 | let highContrast = dynamicColor.highContrast?.nitroColor
11 |
12 | if #available(iOS 13.0, *), (light != nil || dark != nil || highContrast != nil) {
13 | return UIColor { traits in
14 | if traits.accessibilityContrast == .high, let highContrast {
15 | return highContrast
16 | }
17 | switch traits.userInterfaceStyle {
18 | case .dark:
19 | return dark ?? base ?? light ?? UIColor.label
20 | default:
21 | return light ?? base ?? dark ?? UIColor.label
22 | }
23 | }
24 | }
25 |
26 | return base ?? light ?? dark ?? highContrast
27 | }
28 | }
29 |
30 | private extension String {
31 | var nitroColor: UIColor? {
32 | var sanitized = nitroTrimmedNonEmpty ?? ""
33 | if sanitized.hasPrefix("#") {
34 | sanitized.removeFirst()
35 | }
36 |
37 | if sanitized.count == 3 || sanitized.count == 4 {
38 | sanitized = sanitized.reduce(into: "") { partial, character in
39 | partial.append(character)
40 | partial.append(character)
41 | }
42 | }
43 |
44 | guard sanitized.count == 6 || sanitized.count == 8 else {
45 | return nil
46 | }
47 |
48 | var value: UInt64 = 0
49 | guard Scanner(string: sanitized).scanHexInt64(&value) else {
50 | return nil
51 | }
52 |
53 | let hasAlpha = sanitized.count == 8
54 | let alpha = hasAlpha ? CGFloat((value & 0xFF000000) >> 24) / 255.0 : 1.0
55 | let red = CGFloat((value & 0x00FF0000) >> 16) / 255.0
56 | let green = CGFloat((value & 0x0000FF00) >> 8) / 255.0
57 | let blue = CGFloat(value & 0x000000FF) / 255.0
58 |
59 | return UIColor(red: red, green: green, blue: blue, alpha: alpha)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleDisplayName
10 | InappbrowserNitroExample
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | $(MARKETING_VERSION)
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | $(CURRENT_PROJECT_VERSION)
27 | LSRequiresIPhoneOS
28 |
29 | NSAppTransportSecurity
30 |
31 | NSAllowsArbitraryLoads
32 |
33 | NSAllowsLocalNetworking
34 |
35 |
36 | NSLocationWhenInUseUsageDescription
37 |
38 | RCTNewArchEnabled
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | arm64
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UIViewControllerBasedStatusBarAppearance
53 |
54 | CFBundleURLTypes
55 |
56 |
57 | CFBundleURLSchemes
58 |
59 | inappbrowsernitro
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 |
25 | # Use this property to specify which architecture you want to build.
26 | # You can also override it from the CLI using
27 | # ./gradlew -PreactNativeArchitectures=x86_64
28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
29 |
30 | # Use this property to enable support to the new architecture.
31 | # This will allow you to use TurboModules and the Fabric render in
32 | # your application. You should enable this flag either if you want
33 | # to write custom TurboModules/Fabric components OR use libraries that
34 | # are providing them.
35 | newArchEnabled=true
36 |
37 | # Use this property to enable or disable the Hermes JS engine.
38 | # If set to false, you will be using JSC instead.
39 | hermesEnabled=true
40 |
41 | # Use this property to enable edge-to-edge display support.
42 | # This allows your app to draw behind system bars for an immersive UI.
43 | # Note: Only works with ReactActivity and should not be used with custom Activity.
44 | edgeToEdgeEnabled=false
45 |
--------------------------------------------------------------------------------
/.github/workflows/android-build.yml:
--------------------------------------------------------------------------------
1 | name: Build Android
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | paths:
11 | - '.github/workflows/android-build.yml'
12 | - 'example/android/**'
13 | - 'nitrogen/generated/shared/**'
14 | - 'nitrogen/generated/android/**'
15 | - 'cpp/**'
16 | - 'android/**'
17 | - '**/bun.lock'
18 | - '**/react-native.config.js'
19 | - '**/nitro.json'
20 | pull_request:
21 | paths:
22 | - '.github/workflows/android-build.yml'
23 | - 'example/android/**'
24 | - '**/nitrogen/generated/shared/**'
25 | - '**/nitrogen/generated/android/**'
26 | - 'cpp/**'
27 | - 'android/**'
28 | - '**/bun.lock'
29 | - '**/react-native.config.js'
30 | - '**/nitro.json'
31 | workflow_dispatch:
32 |
33 | concurrency:
34 | group: ${{ github.workflow }}-${{ github.ref }}
35 | cancel-in-progress: true
36 |
37 | jobs:
38 | build:
39 | name: Build Android Example App
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: actions/checkout@v4
43 | - uses: actions/setup-node@v4
44 | with:
45 | node-version: '20'
46 |
47 | - name: Install dependencies (yarn)
48 | run: yarn install --immutable
49 |
50 | - name: Generate Nitro bindings
51 | run: yarn codegen
52 |
53 | - name: Setup JDK 17
54 | uses: actions/setup-java@v5
55 | with:
56 | distribution: 'zulu'
57 | java-version: '17'
58 | cache: 'gradle'
59 |
60 | - name: Cache Gradle
61 | uses: actions/cache@v4
62 | with:
63 | path: |
64 | ~/.gradle/caches
65 | ~/.gradle/wrapper
66 | key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/**/*.gradle*') }}
67 | restore-keys: |
68 | ${{ runner.os }}-gradle-
69 |
70 | - name: Run Gradle build
71 | working-directory: example/android
72 | run: ./gradlew assembleDebug --no-daemon --build-cache
73 |
74 | - name: Stop Gradle daemon
75 | working-directory: example/android
76 | run: ./gradlew --stop
77 |
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/browser/DynamicColorResolver.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro.browser
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.view.accessibility.AccessibilityManager
6 | import androidx.core.content.getSystemService
7 | import com.margelo.nitro.inappbrowsernitro.DynamicColor
8 |
9 | internal object DynamicColorResolver {
10 | fun resolve(context: Context, dynamicColor: DynamicColor?): Int? {
11 | dynamicColor ?: return null
12 |
13 | val accessibilityManager = context.getSystemService()
14 | val isHighContrast = accessibilityManager?.let { manager ->
15 | runCatching {
16 | val method = AccessibilityManager::class.java.getMethod("isHighTextContrastEnabled")
17 | (method.invoke(manager) as? Boolean) == true
18 | }.getOrDefault(false)
19 | } == true
20 |
21 | val isDark = (context.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES
22 |
23 | val candidate = when {
24 | isHighContrast && dynamicColor.highContrast != null -> dynamicColor.highContrast
25 | isDark && dynamicColor.dark != null -> dynamicColor.dark
26 | !isDark && dynamicColor.light != null -> dynamicColor.light
27 | dynamicColor.base != null -> dynamicColor.base
28 | else -> dynamicColor.dark ?: dynamicColor.light ?: dynamicColor.highContrast
29 | }
30 |
31 | return candidate?.let(::parseColorSafely)
32 | }
33 |
34 | fun resolveForScheme(dynamicColor: DynamicColor?, preferred: DynamicScheme): Int? {
35 | dynamicColor ?: return null
36 | val hex = when (preferred) {
37 | DynamicScheme.SYSTEM -> dynamicColor.base ?: dynamicColor.light ?: dynamicColor.dark ?: dynamicColor.highContrast
38 | DynamicScheme.LIGHT -> dynamicColor.light ?: dynamicColor.base
39 | DynamicScheme.DARK -> dynamicColor.dark ?: dynamicColor.base
40 | }
41 | return hex?.let(::parseColorSafely)
42 | }
43 |
44 | private fun parseColorSafely(value: String): Int? {
45 | return try {
46 | Color.parseColor(value.trim())
47 | } catch (_: IllegalArgumentException) {
48 | null
49 | }
50 | }
51 |
52 | enum class DynamicScheme { SYSTEM, LIGHT, DARK }
53 | }
54 |
--------------------------------------------------------------------------------
/android/fix-prefab.gradle:
--------------------------------------------------------------------------------
1 | tasks.configureEach { task ->
2 | // Make sure that we generate our prefab publication file only after having built the native library
3 | // so that not a header publication file, but a full configuration publication will be generated, which
4 | // will include the .so file
5 |
6 | def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7 | def matcher = task.name =~ prefabConfigurePattern
8 | if (matcher.matches()) {
9 | def variantName = matcher[0][1]
10 | task.outputs.upToDateWhen { false }
11 | task.dependsOn("externalNativeBuild${variantName}")
12 | }
13 | }
14 |
15 | afterEvaluate {
16 | def abis = reactNativeArchitectures()
17 | rootProject.allprojects.each { proj ->
18 | if (proj === rootProject) return
19 |
20 | def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21 | config.dependencies.any { dep ->
22 | dep.group == project.group && dep.name == project.name
23 | }
24 | }
25 | if (!dependsOnThisLib && proj != project) return
26 |
27 | if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28 | return
29 | }
30 |
31 | def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32 | // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33 | // generate a libnameConfig.cmake file that will contain our native library (.so).
34 | // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35 | variants.all { variant ->
36 | def variantName = variant.name
37 | abis.each { abi ->
38 | def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39 | if (!searchDir.exists()) return
40 | def matches = []
41 | searchDir.eachDir { randomDir ->
42 | def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43 | if (prefabFile.exists()) matches << prefabFile
44 | }
45 | matches.each { prefabConfig ->
46 | prefabConfig.setLastModified(System.currentTimeMillis())
47 | }
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/core/InAppBrowser.ts:
--------------------------------------------------------------------------------
1 | import { NitroModules } from 'react-native-nitro-modules'
2 |
3 | import type {
4 | InappbrowserNitro,
5 | InAppBrowserAuthResult,
6 | InAppBrowserOptions,
7 | InAppBrowserResult,
8 | } from '../specs/inappbrowser-nitro.nitro'
9 | import type { UseInAppBrowserReturn } from '../hooks/useInAppBrowser'
10 | import { normalizeOptions } from '../utils/options'
11 | import { normalizeUrl } from '../utils/url'
12 |
13 | let cachedModule: InappbrowserNitro | null = null
14 |
15 | const getNativeModule = (): InappbrowserNitro => {
16 | if (!cachedModule) {
17 | cachedModule = NitroModules.createHybridObject(
18 | 'InappbrowserNitro'
19 | )
20 | }
21 |
22 | return cachedModule
23 | }
24 |
25 | const mapOptions = (options?: InAppBrowserOptions) => normalizeOptions(options)
26 |
27 | /**
28 | * Public imperative API for the in-app browser Nitro module.
29 | */
30 | export class InAppBrowser {
31 | /** Optional React hook injector (populated in index.ts). */
32 | static useInAppBrowser?: () => UseInAppBrowserReturn
33 |
34 | /**
35 | * Return whether the current device/runtime can present the in-app browser.
36 | */
37 | static async isAvailable(): Promise {
38 | return getNativeModule().isAvailable()
39 | }
40 |
41 | /**
42 | * Launch the in-app browser with the provided URL and options.
43 | */
44 | static async open(
45 | url: string,
46 | options?: InAppBrowserOptions
47 | ): Promise {
48 | const sanitizedUrl = normalizeUrl(url)
49 | const sanitizedOptions = mapOptions(options)
50 | return getNativeModule().open(sanitizedUrl, sanitizedOptions)
51 | }
52 |
53 | /**
54 | * Launch the authentication browser flow, resolving with the redirect payload.
55 | */
56 | static async openAuth(
57 | url: string,
58 | redirectUrl: string,
59 | options?: InAppBrowserOptions
60 | ): Promise {
61 | const sanitizedUrl = normalizeUrl(url)
62 |
63 | const sanitizedRedirect = normalizeUrl(redirectUrl)
64 |
65 | const sanitizedOptions = mapOptions(options)
66 |
67 | return getNativeModule().openAuth(
68 | sanitizedUrl,
69 | sanitizedRedirect,
70 | sanitizedOptions
71 | )
72 | }
73 |
74 | /**
75 | * Close the currently visible browser instance.
76 | */
77 | static async close(): Promise {
78 | return getNativeModule().close()
79 | }
80 |
81 | /**
82 | * Close the currently active authentication session.
83 | */
84 | static async closeAuth(): Promise {
85 | return getNativeModule().closeAuth()
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/src/hooks/useInAppBrowser.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react'
2 |
3 | import { InAppBrowser } from '../core/InAppBrowser'
4 | import type {
5 | InAppBrowserAuthResult,
6 | InAppBrowserOptions,
7 | InAppBrowserResult,
8 | } from '../specs/inappbrowser-nitro.nitro'
9 |
10 | export interface UseInAppBrowserReturn {
11 | open: (
12 | url: string,
13 | options?: InAppBrowserOptions
14 | ) => Promise
15 | openAuth: (
16 | url: string,
17 | redirectUrl: string,
18 | options?: InAppBrowserOptions
19 | ) => Promise
20 | close: () => Promise
21 | closeAuth: () => Promise
22 | isAvailable: () => Promise
23 | isLoading: boolean
24 | error: Error | null
25 | }
26 |
27 | /**
28 | * React hook that wraps the imperative API with loading/error tracking.
29 | */
30 | export function useInAppBrowser(): UseInAppBrowserReturn {
31 | const isMountedRef = useRef(true)
32 |
33 | const [isLoading, setIsLoading] = useState(false)
34 |
35 | const [error, setError] = useState(null)
36 |
37 | const runSafely = useCallback(async (operation: () => Promise) => {
38 | setIsLoading(true)
39 | setError(null)
40 |
41 | try {
42 | const result = await operation()
43 |
44 | if (isMountedRef.current) {
45 | setIsLoading(false)
46 | }
47 |
48 | return result
49 | } catch (err) {
50 | const currentError = err instanceof Error ? err : new Error(String(err))
51 | if (isMountedRef.current) {
52 | setError(currentError)
53 | setIsLoading(false)
54 | }
55 | throw currentError
56 | }
57 | }, [])
58 |
59 | useEffect(() => {
60 | return () => {
61 | isMountedRef.current = false
62 | }
63 | }, [])
64 |
65 | const open = useCallback(
66 | (url: string, options?: InAppBrowserOptions) =>
67 | runSafely(() => InAppBrowser.open(url, options)),
68 | [runSafely]
69 | )
70 |
71 | const openAuth = useCallback(
72 | (url: string, redirectUrl: string, options?: InAppBrowserOptions) =>
73 | runSafely(() => InAppBrowser.openAuth(url, redirectUrl, options)),
74 | [runSafely]
75 | )
76 |
77 | const close = useCallback(() => runSafely(() => InAppBrowser.close()), [runSafely])
78 |
79 | const closeAuth = useCallback(
80 | () => runSafely(() => InAppBrowser.closeAuth()),
81 | [runSafely]
82 | )
83 |
84 | const isAvailable = useCallback(
85 | () => runSafely(() => InAppBrowser.isAvailable()),
86 | [runSafely]
87 | )
88 |
89 | return {
90 | open,
91 | openAuth,
92 | close,
93 | closeAuth,
94 | isAvailable,
95 | isLoading,
96 | error,
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/.github/workflows/ios-build.yml:
--------------------------------------------------------------------------------
1 | name: Build iOS
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | paths:
11 | - '.github/workflows/ios-build.yml'
12 | - 'example/ios/**'
13 | - 'example/Gemfile'
14 | - 'example/Gemfile.lock'
15 | - '**/nitrogen/generated/shared/**'
16 | - '**/nitrogen/generated/ios/**'
17 | - 'cpp/**'
18 | - 'ios/**'
19 | - '**/Podfile.lock'
20 | - '**/bun.lock'
21 | - '**/*.podspec'
22 | - '**/react-native.config.js'
23 | - '**/nitro.json'
24 | pull_request:
25 | paths:
26 | - '.github/workflows/ios-build.yml'
27 | - 'example/ios/**'
28 | - 'example/Gemfile'
29 | - 'example/Gemfile.lock'
30 | - '**/nitrogen/generated/shared/**'
31 | - '**/nitrogen/generated/ios/**'
32 | - 'cpp/**'
33 | - 'ios/**'
34 | - '**/Podfile.lock'
35 | - '**/bun.lock'
36 | - '**/*.podspec'
37 | - '**/react-native.config.js'
38 | - '**/nitro.json'
39 | workflow_dispatch:
40 |
41 | env:
42 | USE_CCACHE: 1
43 |
44 | concurrency:
45 | group: ${{ github.workflow }}-${{ github.ref }}
46 | cancel-in-progress: true
47 |
48 | jobs:
49 | build:
50 | name: Build iOS Example App
51 | runs-on: macOS-15
52 | steps:
53 | - uses: actions/checkout@v4
54 | - uses: actions/setup-node@v4
55 | with:
56 | node-version: '20'
57 | - name: Setup Xcode
58 | uses: maxim-lobanov/setup-xcode@v1
59 | with:
60 | xcode-version: 16.4
61 |
62 | - name: Install dependencies (yarn)
63 | run: yarn install --immutable
64 |
65 | - name: Generate Nitro bindings
66 | run: yarn codegen
67 |
68 | - name: Setup Ruby (bundle)
69 | uses: ruby/setup-ruby@v1
70 | with:
71 | ruby-version: '3.2'
72 | bundler-cache: true
73 | working-directory: example/ios
74 |
75 | - name: Install xcpretty
76 | run: gem install xcpretty
77 |
78 | - name: Cache CocoaPods
79 | uses: actions/cache@v4
80 | with:
81 | path: |
82 | ~/.cocoapods/repos
83 | example/ios/Pods
84 | key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }}
85 | restore-keys: |
86 | ${{ runner.os }}-pods-
87 |
88 | - name: Install Pods
89 | working-directory: example/ios
90 | run: pod install
91 |
92 | - name: Build App
93 | working-directory: example/ios
94 | run: |
95 | set -o pipefail && xcodebuild \
96 | CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \
97 | -derivedDataPath build -UseModernBuildSystem=YES \
98 | -workspace InappbrowserNitroExample.xcworkspace \
99 | -scheme InappbrowserNitroExample \
100 | -sdk iphonesimulator \
101 | -configuration Debug \
102 | -destination 'platform=iOS Simulator,name=iPhone 16' \
103 | build \
104 | CODE_SIGNING_ALLOWED=NO | xcpretty
105 |
--------------------------------------------------------------------------------
/ios/AuthSessionManager.swift:
--------------------------------------------------------------------------------
1 | import AuthenticationServices
2 | import SafariServices
3 |
4 | final class AuthSessionManager: NSObject {
5 | private var session: AuthenticationSession?
6 |
7 | @MainActor
8 | func start(urlString: String, redirectUrl: String, options: InAppBrowserOptions?) async -> InAppBrowserAuthResult {
9 | guard let url = URL(string: urlString) else {
10 | return InAppBrowserAuthResult(type: .dismiss, url: nil, message: "invalid url")
11 | }
12 |
13 | let callbackScheme = URL(string: redirectUrl)?.scheme ?? redirectUrl
14 |
15 | return await withCheckedContinuation { continuation in
16 | if #available(iOS 12.0, *) {
17 | let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme) { [weak self] callbackURL, error in
18 | continuation.resume(returning: self?.mapAuthResult(callbackURL: callbackURL, error: error) ?? Self.genericFailure)
19 | }
20 |
21 | if #available(iOS 13.0, *) {
22 | session.presentationContextProvider = AuthPresentationContextProvider()
23 | session.prefersEphemeralWebBrowserSession = options?.ephemeralWebSession ?? false
24 | }
25 |
26 | // iOS Simulator does not fully emulate Secure Enclave behaviour; expect reduced isolation for ephemeral sessions.
27 | self.session = .asWeb(session)
28 | session.start()
29 | } else {
30 | let session = SFAuthenticationSession(url: url, callbackURLScheme: callbackScheme) { [weak self] callbackURL, error in
31 | continuation.resume(returning: self?.mapAuthResult(callbackURL: callbackURL, error: error) ?? Self.genericFailure)
32 | }
33 | self.session = .sf(session)
34 | session.start()
35 | }
36 | }
37 | }
38 |
39 | @MainActor
40 | func cancel() {
41 | switch session {
42 | case .asWeb(let session):
43 | session.cancel()
44 | case .sf(let session):
45 | session.cancel()
46 | case .none:
47 | break
48 | }
49 | session = nil
50 | }
51 |
52 | private func mapAuthResult(callbackURL: URL?, error: Error?) -> InAppBrowserAuthResult {
53 | if let error {
54 | if #available(iOS 12.0, *), let authError = error as? ASWebAuthenticationSessionError, authError.code == .canceledLogin {
55 | return InAppBrowserAuthResult(type: .cancel, url: nil, message: nil)
56 | }
57 | if #available(iOS 11.0, *), let authError = error as? SFAuthenticationError, authError.code == .canceledLogin {
58 | return InAppBrowserAuthResult(type: .cancel, url: nil, message: nil)
59 | }
60 | return InAppBrowserAuthResult(type: .dismiss, url: nil, message: error.localizedDescription)
61 | }
62 |
63 | if let url = callbackURL?.absoluteString {
64 | return InAppBrowserAuthResult(type: .success, url: url, message: nil)
65 | }
66 |
67 | return Self.genericFailure
68 | }
69 |
70 | private static let genericFailure = InAppBrowserAuthResult(type: .dismiss, url: nil, message: "authentication failed")
71 | }
72 |
73 | @available(iOS 13.0, *)
74 | private final class AuthPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
75 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
76 | UIApplication.shared.nitroTopMostViewController?.view.window ?? UIWindow()
77 | }
78 | }
79 |
80 | private enum AuthenticationSession {
81 | case asWeb(ASWebAuthenticationSession)
82 | case sf(SFAuthenticationSession)
83 | }
84 |
--------------------------------------------------------------------------------
/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @REM Copyright (c) Meta Platforms, Inc. and affiliates.
2 | @REM
3 | @REM This source code is licensed under the MIT license found in the
4 | @REM LICENSE file in the root directory of this source tree.
5 |
6 | @rem
7 | @rem Copyright 2015 the original author or authors.
8 | @rem
9 | @rem Licensed under the Apache License, Version 2.0 (the "License");
10 | @rem you may not use this file except in compliance with the License.
11 | @rem You may obtain a copy of the License at
12 | @rem
13 | @rem https://www.apache.org/licenses/LICENSE-2.0
14 | @rem
15 | @rem Unless required by applicable law or agreed to in writing, software
16 | @rem distributed under the License is distributed on an "AS IS" BASIS,
17 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | @rem See the License for the specific language governing permissions and
19 | @rem limitations under the License.
20 | @rem
21 | @rem SPDX-License-Identifier: Apache-2.0
22 | @rem
23 |
24 | @if "%DEBUG%"=="" @echo off
25 | @rem ##########################################################################
26 | @rem
27 | @rem Gradle startup script for Windows
28 | @rem
29 | @rem ##########################################################################
30 |
31 | @rem Set local scope for the variables with windows NT shell
32 | if "%OS%"=="Windows_NT" setlocal
33 |
34 | set DIRNAME=%~dp0
35 | if "%DIRNAME%"=="" set DIRNAME=.
36 | @rem This is normally unused
37 | set APP_BASE_NAME=%~n0
38 | set APP_HOME=%DIRNAME%
39 |
40 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
41 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
42 |
43 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
44 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
45 |
46 | @rem Find java.exe
47 | if defined JAVA_HOME goto findJavaFromJavaHome
48 |
49 | set JAVA_EXE=java.exe
50 | %JAVA_EXE% -version >NUL 2>&1
51 | if %ERRORLEVEL% equ 0 goto execute
52 |
53 | echo. 1>&2
54 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
55 | echo. 1>&2
56 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
57 | echo location of your Java installation. 1>&2
58 |
59 | goto fail
60 |
61 | :findJavaFromJavaHome
62 | set JAVA_HOME=%JAVA_HOME:"=%
63 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
64 |
65 | if exist "%JAVA_EXE%" goto execute
66 |
67 | echo. 1>&2
68 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
69 | echo. 1>&2
70 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
71 | echo location of your Java installation. 1>&2
72 |
73 | goto fail
74 |
75 | :execute
76 | @rem Setup the command line
77 |
78 | set CLASSPATH=
79 |
80 |
81 | @rem Execute Gradle
82 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
83 |
84 | :end
85 | @rem End local scope for the variables with windows NT shell
86 | if %ERRORLEVEL% equ 0 goto mainEnd
87 |
88 | :fail
89 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
90 | rem the _cmd.exe /c_ return code!
91 | set EXIT_CODE=%ERRORLEVEL%
92 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
93 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
94 | exit /b %EXIT_CODE%
95 |
96 | :mainEnd
97 | if "%OS%"=="Windows_NT" endlocal
98 |
99 | :omega
100 |
--------------------------------------------------------------------------------
/example/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | activesupport (7.2.2.2)
9 | base64
10 | benchmark (>= 0.3)
11 | bigdecimal
12 | concurrent-ruby (~> 1.0, >= 1.3.1)
13 | connection_pool (>= 2.2.5)
14 | drb
15 | i18n (>= 1.6, < 2)
16 | logger (>= 1.4.2)
17 | minitest (>= 5.1)
18 | securerandom (>= 0.3)
19 | tzinfo (~> 2.0, >= 2.0.5)
20 | addressable (2.8.7)
21 | public_suffix (>= 2.0.2, < 7.0)
22 | algoliasearch (1.27.5)
23 | httpclient (~> 2.8, >= 2.8.3)
24 | json (>= 1.5.1)
25 | atomos (0.1.3)
26 | base64 (0.3.0)
27 | benchmark (0.4.1)
28 | bigdecimal (3.3.1)
29 | claide (1.1.0)
30 | cocoapods (1.15.2)
31 | addressable (~> 2.8)
32 | claide (>= 1.0.2, < 2.0)
33 | cocoapods-core (= 1.15.2)
34 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
35 | cocoapods-downloader (>= 2.1, < 3.0)
36 | cocoapods-plugins (>= 1.0.0, < 2.0)
37 | cocoapods-search (>= 1.0.0, < 2.0)
38 | cocoapods-trunk (>= 1.6.0, < 2.0)
39 | cocoapods-try (>= 1.1.0, < 2.0)
40 | colored2 (~> 3.1)
41 | escape (~> 0.0.4)
42 | fourflusher (>= 2.3.0, < 3.0)
43 | gh_inspector (~> 1.0)
44 | molinillo (~> 0.8.0)
45 | nap (~> 1.0)
46 | ruby-macho (>= 2.3.0, < 3.0)
47 | xcodeproj (>= 1.23.0, < 2.0)
48 | cocoapods-core (1.15.2)
49 | activesupport (>= 5.0, < 8)
50 | addressable (~> 2.8)
51 | algoliasearch (~> 1.0)
52 | concurrent-ruby (~> 1.1)
53 | fuzzy_match (~> 2.0.4)
54 | nap (~> 1.0)
55 | netrc (~> 0.11)
56 | public_suffix (~> 4.0)
57 | typhoeus (~> 1.0)
58 | cocoapods-deintegrate (1.0.5)
59 | cocoapods-downloader (2.1)
60 | cocoapods-plugins (1.0.0)
61 | nap
62 | cocoapods-search (1.0.1)
63 | cocoapods-trunk (1.6.0)
64 | nap (>= 0.8, < 2.0)
65 | netrc (~> 0.11)
66 | cocoapods-try (1.2.0)
67 | colored2 (3.1.2)
68 | concurrent-ruby (1.3.3)
69 | connection_pool (2.5.4)
70 | drb (2.2.3)
71 | escape (0.0.4)
72 | ethon (0.15.0)
73 | ffi (>= 1.15.0)
74 | ffi (1.17.2)
75 | fourflusher (2.3.1)
76 | fuzzy_match (2.0.4)
77 | gh_inspector (1.1.3)
78 | httpclient (2.9.0)
79 | mutex_m
80 | i18n (1.14.7)
81 | concurrent-ruby (~> 1.0)
82 | json (2.15.1)
83 | logger (1.7.0)
84 | minitest (5.26.0)
85 | molinillo (0.8.0)
86 | mutex_m (0.3.0)
87 | nanaimo (0.3.0)
88 | nap (1.1.0)
89 | netrc (0.11.0)
90 | nkf (0.2.0)
91 | public_suffix (4.0.7)
92 | rexml (3.4.4)
93 | ruby-macho (2.5.1)
94 | securerandom (0.4.1)
95 | typhoeus (1.5.0)
96 | ethon (>= 0.9.0, < 0.16.0)
97 | tzinfo (2.0.6)
98 | concurrent-ruby (~> 1.0)
99 | xcodeproj (1.25.1)
100 | CFPropertyList (>= 2.3.3, < 4.0)
101 | atomos (~> 0.1.3)
102 | claide (>= 1.0.2, < 2.0)
103 | colored2 (~> 3.1)
104 | nanaimo (~> 0.3.0)
105 | rexml (>= 3.3.6, < 4.0)
106 |
107 | PLATFORMS
108 | ruby
109 |
110 | DEPENDENCIES
111 | activesupport (>= 6.1.7.5, != 7.1.0)
112 | benchmark
113 | bigdecimal
114 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
115 | concurrent-ruby (< 1.3.4)
116 | logger
117 | mutex_m
118 | xcodeproj (< 1.26.0)
119 |
120 | RUBY VERSION
121 | ruby 3.4.7p58
122 |
123 | BUNDLED WITH
124 | 2.7.2
125 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample.xcodeproj/xcshareddata/xcschemes/InAppBrowserNitroExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
2 |
3 | # Getting Started
4 |
5 | > **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
6 |
7 | ## Step 1: Start Metro
8 |
9 | First, you will need to run **Metro**, the JavaScript build tool for React Native.
10 |
11 | To start the Metro dev server, run the following command from the root of your React Native project:
12 |
13 | ```sh
14 | # Using npm
15 | npm start
16 |
17 | # OR using Yarn
18 | yarn start
19 | ```
20 |
21 | ## Step 2: Build and run your app
22 |
23 | With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
24 |
25 | ### Android
26 |
27 | ```sh
28 | # Using npm
29 | npm run android
30 |
31 | # OR using Yarn
32 | yarn android
33 | ```
34 |
35 | ### iOS
36 |
37 | For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
38 |
39 | The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
40 |
41 | ```sh
42 | bundle install
43 | ```
44 |
45 | Then, and every time you update your native dependencies, run:
46 |
47 | ```sh
48 | bundle exec pod install
49 | ```
50 |
51 | For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
52 |
53 | ```sh
54 | # Using npm
55 | npm run ios
56 |
57 | # OR using Yarn
58 | yarn ios
59 | ```
60 |
61 | If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
62 |
63 | This is one way to run your app — you can also build it directly from Android Studio or Xcode.
64 |
65 | ## Step 3: Modify your app
66 |
67 | Now that you have successfully run the app, let's make changes!
68 |
69 | Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
70 |
71 | When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
72 |
73 | - **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS).
74 | - **iOS**: Press R in iOS Simulator.
75 |
76 | ## Congratulations! :tada:
77 |
78 | You've successfully run and modified your React Native App. :partying_face:
79 |
80 | ### Now what?
81 |
82 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
83 | - If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
84 |
85 | # Troubleshooting
86 |
87 | If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
88 |
89 | # Learn More
90 |
91 | To learn more about React Native, take a look at the following resources:
92 |
93 | - [React Native Website](https://reactnative.dev) - learn more about React Native.
94 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
95 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
96 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
97 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-inappbrowser-nitro",
3 | "version": "2.1.2",
4 | "description": "react-native-inappbrowser-nitro is a react native package built with Nitro",
5 | "main": "./lib/commonjs/index.js",
6 | "module": "./lib/module/index.js",
7 | "types": "./lib/typescript/src/index.d.ts",
8 | "react-native": "src/index",
9 | "source": "src/index",
10 | "scripts": {
11 | "typecheck": "tsc --noEmit",
12 | "clean": "git clean -dfX",
13 | "release": "semantic-release",
14 | "build": "npm run typecheck && bob build && node ./scripts/remove-source-maps.cjs",
15 | "codegen": "nitrogen --logLevel=\"debug\" && npm run build && node post-script.js",
16 | "prepublish": "yarn codegen"
17 | },
18 | "keywords": [
19 | "react-native",
20 | "in-app-browser",
21 | "custom-tabs",
22 | "safari-view-controller",
23 | "webview",
24 | "browser",
25 | "nitro",
26 | "android",
27 | "ios",
28 | "oauth",
29 | "authentication",
30 | "sso",
31 | "chrome-custom-tabs",
32 | "safari",
33 | "react-native-inappbrowser-nitro"
34 | ],
35 | "files": [
36 | "src",
37 | "react-native.config.js",
38 | "lib",
39 | "nitrogen",
40 | "cpp",
41 | "nitro.json",
42 | "android/build.gradle",
43 | "android/fix-prefab.gradle",
44 | "android/gradle.properties",
45 | "android/CMakeLists.txt",
46 | "android/src",
47 | "ios/**/*.h",
48 | "ios/**/*.m",
49 | "ios/**/*.mm",
50 | "ios/**/*.cpp",
51 | "ios/**/*.swift",
52 | "app.plugin.js",
53 | "*.podspec",
54 | "README.md"
55 | ],
56 | "workspaces": [
57 | "example"
58 | ],
59 | "repository": "https://github.com/mCodex/react-native-inappbrowser-nitro.git",
60 | "author": "Mateus Andrade",
61 | "license": "MIT",
62 | "bugs": "https://github.com/mCodex/react-native-inappbrowser-nitro/issues",
63 | "homepage": "https://github.com/mCodex/react-native-inappbrowser-nitro#readme",
64 | "publishConfig": {
65 | "access": "public",
66 | "registry": "https://registry.npmjs.org/"
67 | },
68 | "devDependencies": {
69 | "@jamesacarr/eslint-formatter-github-actions": "^0.2.0",
70 | "@semantic-release/changelog": "^6.0.3",
71 | "@semantic-release/git": "^10.0.1",
72 | "@types/jest": "^30.0.0",
73 | "@types/react": "19.2.7",
74 | "conventional-changelog-conventionalcommits": "^9.1.0",
75 | "nitrogen": "0.31.10",
76 | "react": "19.2.3",
77 | "react-native": "0.83",
78 | "react-native-builder-bob": "^0.40.17",
79 | "react-native-nitro-modules": "0.31.10",
80 | "semantic-release": "^25.0.2",
81 | "typescript": "^5.9.3"
82 | },
83 | "peerDependencies": {
84 | "react": "*",
85 | "react-native": "*",
86 | "react-native-nitro-modules": "*"
87 | },
88 | "eslintConfig": {
89 | "root": true,
90 | "extends": [
91 | "@react-native",
92 | "prettier"
93 | ],
94 | "plugins": [
95 | "prettier"
96 | ],
97 | "rules": {
98 | "prettier/prettier": [
99 | "warn",
100 | {
101 | "quoteProps": "consistent",
102 | "singleQuote": true,
103 | "tabWidth": 2,
104 | "trailingComma": "es5",
105 | "useTabs": false
106 | }
107 | ]
108 | }
109 | },
110 | "eslintIgnore": [
111 | "node_modules/",
112 | "lib/"
113 | ],
114 | "prettier": {
115 | "quoteProps": "consistent",
116 | "singleQuote": true,
117 | "tabWidth": 2,
118 | "trailingComma": "es5",
119 | "useTabs": false,
120 | "semi": false
121 | },
122 | "react-native-builder-bob": {
123 | "source": "src",
124 | "output": "lib",
125 | "targets": [
126 | "commonjs",
127 | "module",
128 | [
129 | "typescript",
130 | {
131 | "project": "tsconfig.json"
132 | }
133 | ]
134 | ]
135 | },
136 | "packageManager": "yarn@4.11.0"
137 | }
138 |
--------------------------------------------------------------------------------
/src/utils/options.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BrowserAnimations,
3 | DynamicColor,
4 | InAppBrowserOptions,
5 | } from '../specs/inappbrowser-nitro.nitro'
6 |
7 | const COLOR_OPTION_KEYS = new Set([
8 | 'preferredBarTintColor',
9 | 'preferredControlTintColor',
10 | 'toolbarColor',
11 | 'secondaryToolbarColor',
12 | 'navigationBarColor',
13 | 'navigationBarDividerColor',
14 | ])
15 |
16 | const sanitizeColor = (value: DynamicColor | string | undefined) => {
17 | if (!value) {
18 | return undefined
19 | }
20 |
21 | if (typeof value === 'string') {
22 | const trimmed = value.trim()
23 | return trimmed ? { base: trimmed } : undefined
24 | }
25 |
26 | const payload: DynamicColor = {}
27 |
28 | if (value.base?.trim()) {
29 | payload.base = value.base.trim()
30 | }
31 | if (value.light?.trim()) {
32 | payload.light = value.light.trim()
33 | }
34 | if (value.dark?.trim()) {
35 | payload.dark = value.dark.trim()
36 | }
37 | if (value.highContrast?.trim()) {
38 | payload.highContrast = value.highContrast.trim()
39 | }
40 |
41 | return Object.keys(payload).length > 0 ? payload : undefined
42 | }
43 |
44 | const sanitizeAnimations = (animations?: BrowserAnimations) => {
45 | if (!animations) {
46 | return undefined
47 | }
48 |
49 | const sanitized: BrowserAnimations = {}
50 | if (animations.startEnter?.trim()) {
51 | sanitized.startEnter = animations.startEnter.trim()
52 | }
53 | if (animations.startExit?.trim()) {
54 | sanitized.startExit = animations.startExit.trim()
55 | }
56 | if (animations.endEnter?.trim()) {
57 | sanitized.endEnter = animations.endEnter.trim()
58 | }
59 | if (animations.endExit?.trim()) {
60 | sanitized.endExit = animations.endExit.trim()
61 | }
62 |
63 | return Object.keys(sanitized).length > 0 ? sanitized : undefined
64 | }
65 |
66 | const sanitizeHeaders = (headers?: Record) => {
67 | if (!headers) {
68 | return undefined
69 | }
70 |
71 | const sanitized: Record = {}
72 | for (const [key, currentValue] of Object.entries(headers)) {
73 | if (typeof currentValue !== 'string') {
74 | continue
75 | }
76 |
77 | const normalizedKey = key.trim()
78 | if (!normalizedKey) {
79 | continue
80 | }
81 |
82 | sanitized[normalizedKey] = currentValue
83 | }
84 |
85 | return Object.keys(sanitized).length > 0 ? sanitized : undefined
86 | }
87 |
88 | /**
89 | * Remove undefined entries and sanitize nested values before hitting native.
90 | */
91 | export const normalizeOptions = (options?: InAppBrowserOptions) => {
92 | if (!options) {
93 | return undefined
94 | }
95 |
96 | const sanitized: InAppBrowserOptions = {}
97 |
98 | for (const [key, value] of Object.entries(options)) {
99 | if (value === undefined || value === null) {
100 | continue
101 | }
102 |
103 | if (COLOR_OPTION_KEYS.has(key as keyof InAppBrowserOptions)) {
104 | const normalizedColor = sanitizeColor(value as string | DynamicColor)
105 | if (normalizedColor) {
106 | // @ts-expect-error - dynamic key assignment is safe for optional props
107 | sanitized[key] = normalizedColor
108 | }
109 | continue
110 | }
111 |
112 | if (key === 'headers') {
113 | const normalizedHeaders = sanitizeHeaders(value as Record)
114 | if (normalizedHeaders) {
115 | sanitized.headers = normalizedHeaders
116 | }
117 | continue
118 | }
119 |
120 | if (key === 'animations') {
121 | const normalizedAnimations = sanitizeAnimations(value as BrowserAnimations)
122 | if (normalizedAnimations) {
123 | sanitized.animations = normalizedAnimations
124 | }
125 | continue
126 | }
127 |
128 | // @ts-expect-error - dynamic key assignment is safe for optional props
129 | sanitized[key] = value
130 | }
131 |
132 | return Object.keys(sanitized).length > 0 ? sanitized : undefined
133 | }
134 |
--------------------------------------------------------------------------------
/example/ios/InAppBrowserNitroExample/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 |
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:8.8.0"
9 | }
10 | }
11 |
12 | def reactNativeArchitectures() {
13 | def value = rootProject.getProperties().get("reactNativeArchitectures")
14 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
15 | }
16 |
17 | def isNewArchitectureEnabled() {
18 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19 | }
20 |
21 | apply plugin: "com.android.library"
22 | apply plugin: 'org.jetbrains.kotlin.android'
23 | apply from: '../nitrogen/generated/android/InappbrowserNitro+autolinking.gradle'
24 | apply from: "./fix-prefab.gradle"
25 |
26 | if (isNewArchitectureEnabled()) {
27 | apply plugin: "com.facebook.react"
28 | }
29 |
30 | def getExtOrDefault(name) {
31 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["InappbrowserNitro_" + name]
32 | }
33 |
34 | def getExtOrIntegerDefault(name) {
35 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["InappbrowserNitro_" + name]).toInteger()
36 | }
37 |
38 | android {
39 | namespace "com.inappbrowsernitro"
40 |
41 | ndkVersion getExtOrDefault("ndkVersion")
42 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
43 |
44 | defaultConfig {
45 | minSdkVersion getExtOrIntegerDefault("minSdkVersion")
46 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
47 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
48 |
49 | externalNativeBuild {
50 | cmake {
51 | cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
52 | arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
53 | abiFilters (*reactNativeArchitectures())
54 |
55 | buildTypes {
56 | debug {
57 | cppFlags "-O1 -g"
58 | }
59 | release {
60 | cppFlags "-O2"
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | externalNativeBuild {
68 | cmake {
69 | path "CMakeLists.txt"
70 | }
71 | }
72 |
73 | packagingOptions {
74 | excludes = [
75 | "META-INF",
76 | "META-INF/**",
77 | "**/libc++_shared.so",
78 | "**/libfbjni.so",
79 | "**/libjsi.so",
80 | "**/libfolly_json.so",
81 | "**/libfolly_runtime.so",
82 | "**/libglog.so",
83 | "**/libhermes.so",
84 | "**/libhermes-executor-debug.so",
85 | "**/libhermes_executor.so",
86 | "**/libreactnative.so",
87 | "**/libreactnativejni.so",
88 | "**/libturbomodulejsijni.so",
89 | "**/libreact_nativemodule_core.so",
90 | "**/libjscexecutor.so"
91 | ]
92 | }
93 |
94 | buildFeatures {
95 | buildConfig true
96 | prefab true
97 | }
98 |
99 | buildTypes {
100 | release {
101 | minifyEnabled false
102 | }
103 | }
104 |
105 | lintOptions {
106 | disable "GradleCompatible"
107 | }
108 |
109 | compileOptions {
110 | sourceCompatibility JavaVersion.VERSION_1_8
111 | targetCompatibility JavaVersion.VERSION_1_8
112 | }
113 |
114 | sourceSets {
115 | main {
116 | if (isNewArchitectureEnabled()) {
117 | java.srcDirs += [
118 | // React Codegen files
119 | "${project.buildDir}/generated/source/codegen/java"
120 | ]
121 | }
122 | }
123 | }
124 | }
125 |
126 | repositories {
127 | mavenCentral()
128 | google()
129 | }
130 |
131 |
132 | dependencies {
133 | // For < 0.71, this will be from the local maven repo
134 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
135 | //noinspection GradleDynamicVersion
136 | implementation "com.facebook.react:react-native:+"
137 |
138 | // Add a dependency on NitroModules
139 | implementation project(":react-native-nitro-modules")
140 |
141 | implementation "androidx.browser:browser:1.7.0"
142 | implementation "androidx.core:core-ktx:1.13.1"
143 | }
144 |
145 | if (isNewArchitectureEnabled()) {
146 | react {
147 | jsRootDir = file("../src/")
148 | libraryName = "InappbrowserNitro"
149 | codegenJavaPackageName = "com.inappbrowsernitro"
150 | }
151 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/HybridInappbrowserNitro.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.net.Uri
8 | import androidx.browser.customtabs.CustomTabsIntent
9 | import com.inappbrowsernitro.browser.BrowserFallback
10 | import com.inappbrowsernitro.browser.CustomTabsIntentFactory
11 | import com.inappbrowsernitro.browser.CustomTabsPackageHelper
12 | import com.margelo.nitro.NitroModules
13 | import com.margelo.nitro.core.Promise
14 | import com.margelo.nitro.inappbrowsernitro.BrowserResultType
15 | import com.margelo.nitro.inappbrowsernitro.HybridInappbrowserNitroSpec
16 | import com.margelo.nitro.inappbrowsernitro.InAppBrowserAuthResult
17 | import com.margelo.nitro.inappbrowsernitro.InAppBrowserOptions
18 | import com.margelo.nitro.inappbrowsernitro.InAppBrowserResult
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.withContext
21 |
22 | class HybridInappbrowserNitro : HybridInappbrowserNitroSpec() {
23 | private val reactContext get() = NitroModules.applicationContext
24 | private val applicationContext: Context?
25 | get() = reactContext ?: NitroModules.applicationContext
26 |
27 | override fun isAvailable(): Promise {
28 | val context = applicationContext ?: return Promise.resolved(false)
29 | val customTabsPackage = CustomTabsPackageHelper.resolvePackage(context, null)
30 | if (customTabsPackage != null) {
31 | return Promise.resolved(true)
32 | }
33 |
34 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(SCHEME_CHECK_URL))
35 | val canHandle = intent.resolveActivity(context.packageManager) != null
36 | return Promise.resolved(canHandle)
37 | }
38 |
39 | override fun open(url: String, options: InAppBrowserOptions?): Promise {
40 | return Promise.async {
41 | openInternal(url, options)
42 | }
43 | }
44 |
45 | override fun openAuth(url: String, redirectUrl: String, options: InAppBrowserOptions?): Promise {
46 | return Promise.async {
47 | val result = openInternal(url, options)
48 | InAppBrowserAuthResult(result.type, result.url, result.message)
49 | }
50 | }
51 |
52 | override fun close(): Promise {
53 | return Promise.resolved(Unit)
54 | }
55 |
56 | override fun closeAuth(): Promise {
57 | return Promise.resolved(Unit)
58 | }
59 |
60 | private suspend fun openInternal(url: String, options: InAppBrowserOptions?): InAppBrowserResult {
61 | val context = applicationContext ?: return dismiss("React context unavailable")
62 | val parsedUri = runCatching { Uri.parse(url) }.getOrNull()
63 | ?: return dismiss("Invalid URL: $url")
64 |
65 | val customTabsPackage = CustomTabsPackageHelper.resolvePackage(context, null)
66 | val launchContext = reactContext?.currentActivity ?: context
67 |
68 | if (customTabsPackage == "com.android.chrome") {
69 | val intent = CustomTabsIntentFactory(context, null).create(options)
70 | intent.intent.setPackage(customTabsPackage)
71 | val launched = launchCustomTab(intent, launchContext, parsedUri)
72 | if (launched) {
73 | return InAppBrowserResult(BrowserResultType.SUCCESS, parsedUri.toString(), null)
74 | }
75 | }
76 |
77 | val fallbackLaunched = launchFallback(launchContext, url)
78 | return if (fallbackLaunched) {
79 | InAppBrowserResult(BrowserResultType.SUCCESS, parsedUri.toString(), null)
80 | } else {
81 | dismiss("No browser available to handle $url")
82 | }
83 | }
84 |
85 | private suspend fun launchCustomTab(intent: CustomTabsIntent, context: Context, uri: Uri): Boolean {
86 | return withContext(Dispatchers.Main) {
87 | try {
88 | if (context !is Activity) {
89 | intent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
90 | }
91 | intent.launchUrl(context, uri)
92 | true
93 | } catch (_: ActivityNotFoundException) {
94 | false
95 | }
96 | }
97 | }
98 |
99 | private fun launchFallback(context: Context, url: String): Boolean {
100 | val fallback = BrowserFallback(context)
101 | if (fallback.openSystemBrowser(url)) return true
102 | if (fallback.openChooser(url)) return true
103 | return fallback.redirectToStore()
104 | }
105 |
106 | private fun dismiss(message: String): InAppBrowserResult {
107 | return InAppBrowserResult(BrowserResultType.DISMISS, null, message)
108 | }
109 |
110 | private companion object {
111 | private const val SCHEME_CHECK_URL = "https://example.com"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "org.jetbrains.kotlin.android"
3 | apply plugin: "com.facebook.react"
4 |
5 | /**
6 | * This is the configuration block to customize your React Native Android app.
7 | * By default you don't need to apply any configuration, just uncomment the lines you need.
8 | */
9 | react {
10 | /* Folders */
11 | // The root of your project, i.e. where "package.json" lives. Default is '../..'
12 | // root = file("../../")
13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
14 | reactNativeDir = file("../../../node_modules/react-native")
15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
16 | codegenDir = file("../../../node_modules/@react-native/codegen")
17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
18 | cliFile = file("../../../node_modules/react-native/cli.js")
19 |
20 | /* Variants */
21 | // The list of variants to that are debuggable. For those we're going to
22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
24 | // debuggableVariants = ["liteDebug", "prodDebug"]
25 |
26 | /* Bundling */
27 | // A list containing the node command and its flags. Default is just 'node'.
28 | // nodeExecutableAndArgs = ["node"]
29 | //
30 | // The command to run when bundling. By default is 'bundle'
31 | // bundleCommand = "ram-bundle"
32 | //
33 | // The path to the CLI configuration file. Default is empty.
34 | // bundleConfig = file(../rn-cli.config.js)
35 | //
36 | // The name of the generated asset file containing your JS bundle
37 | // bundleAssetName = "MyApplication.android.bundle"
38 | //
39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
40 | // entryFile = file("../js/MyApplication.android.js")
41 | //
42 | // A list of extra flags to pass to the 'bundle' commands.
43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
44 | // extraPackagerArgs = []
45 |
46 | /* Hermes Commands */
47 | // The hermes compiler command to run. By default it is 'hermesc'
48 | hermesCommand = "$rootDir/../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
49 | //
50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51 | // hermesFlags = ["-O", "-output-source-map"]
52 |
53 | /* Autolinking */
54 | autolinkLibrariesWithApp()
55 | }
56 |
57 | /**
58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
59 | */
60 | def enableProguardInReleaseBuilds = false
61 |
62 | /**
63 | * The preferred build flavor of JavaScriptCore (JSC)
64 | *
65 | * For example, to use the international variant, you can use:
66 | * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
67 | *
68 | * The international variant includes ICU i18n library and necessary data
69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
70 | * give correct results when using with locales other than en-US. Note that
71 | * this variant is about 6MiB larger per architecture than default.
72 | */
73 | def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
74 |
75 | android {
76 | ndkVersion rootProject.ext.ndkVersion
77 | buildToolsVersion rootProject.ext.buildToolsVersion
78 | compileSdk rootProject.ext.compileSdkVersion
79 |
80 | namespace "com.inappbrowsernitroexample"
81 | defaultConfig {
82 | applicationId "com.inappbrowsernitroexample"
83 | minSdkVersion rootProject.ext.minSdkVersion
84 | targetSdkVersion rootProject.ext.targetSdkVersion
85 | versionCode 1
86 | versionName "1.0"
87 | }
88 | signingConfigs {
89 | debug {
90 | storeFile file('debug.keystore')
91 | storePassword 'android'
92 | keyAlias 'androiddebugkey'
93 | keyPassword 'android'
94 | }
95 | }
96 | buildTypes {
97 | debug {
98 | signingConfig signingConfigs.debug
99 | }
100 | release {
101 | // Caution! In production, you need to generate your own keystore file.
102 | // see https://reactnative.dev/docs/signed-apk-android.
103 | signingConfig signingConfigs.debug
104 | minifyEnabled enableProguardInReleaseBuilds
105 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
106 | }
107 | }
108 | }
109 |
110 | dependencies {
111 | // The version of react-native is set by the React Native Gradle Plugin
112 | implementation("com.facebook.react:react-android")
113 |
114 | if (hermesEnabled.toBoolean()) {
115 | implementation("com.facebook.react:hermes-android")
116 | } else {
117 | implementation jscFlavor
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/specs/inappbrowser-nitro.nitro.ts:
--------------------------------------------------------------------------------
1 | import { type HybridObject } from 'react-native-nitro-modules'
2 |
3 | /**
4 | * Discrete result types returned by the native browser implementations.
5 | */
6 | export const BrowserResultType = {
7 | /** User actively dismissed the browser (tap on Done/Close/back). */
8 | Cancel: 'cancel',
9 | /** Browser closed due to an error or system level interruption. */
10 | Dismiss: 'dismiss',
11 | /** Browser launched successfully. */
12 | Success: 'success',
13 | } as const
14 |
15 | export type BrowserResultType =
16 | (typeof BrowserResultType)[keyof typeof BrowserResultType]
17 |
18 | /**
19 | * iOS dismiss button appearance options.
20 | */
21 | export const DismissButtonStyle = {
22 | Done: 'done',
23 | Close: 'close',
24 | Cancel: 'cancel',
25 | } as const
26 |
27 | export type DismissButtonStyle =
28 | (typeof DismissButtonStyle)[keyof typeof DismissButtonStyle]
29 |
30 | /**
31 | * iOS presentation styles exposed by Safari Services.
32 | */
33 | export const ModalPresentationStyle = {
34 | Automatic: 'automatic',
35 | None: 'none',
36 | FullScreen: 'fullScreen',
37 | PageSheet: 'pageSheet',
38 | FormSheet: 'formSheet',
39 | CurrentContext: 'currentContext',
40 | Custom: 'custom',
41 | OverFullScreen: 'overFullScreen',
42 | OverCurrentContext: 'overCurrentContext',
43 | Popover: 'popover',
44 | } as const
45 |
46 | export type ModalPresentationStyle =
47 | (typeof ModalPresentationStyle)[keyof typeof ModalPresentationStyle]
48 |
49 | /**
50 | * iOS transition styles available when presenting Safari.
51 | */
52 | export const ModalTransitionStyle = {
53 | CoverVertical: 'coverVertical',
54 | FlipHorizontal: 'flipHorizontal',
55 | CrossDissolve: 'crossDissolve',
56 | PartialCurl: 'partialCurl',
57 | } as const
58 |
59 | export type ModalTransitionStyle =
60 | (typeof ModalTransitionStyle)[keyof typeof ModalTransitionStyle]
61 |
62 | /**
63 | * Android Custom Tabs color scheme modes.
64 | */
65 | export const BrowserColorScheme = {
66 | System: 'system',
67 | Light: 'light',
68 | Dark: 'dark',
69 | } as const
70 |
71 | export type BrowserColorScheme =
72 | (typeof BrowserColorScheme)[keyof typeof BrowserColorScheme]
73 |
74 | /**
75 | * Android Custom Tabs share state visibility.
76 | */
77 | export const BrowserShareState = {
78 | Default: 'default',
79 | On: 'on',
80 | Off: 'off',
81 | } as const
82 |
83 | export type BrowserShareState =
84 | (typeof BrowserShareState)[keyof typeof BrowserShareState]
85 |
86 | export const StatusBarStyle = {
87 | Default: 'default',
88 | LightContent: 'lightContent',
89 | DarkContent: 'darkContent',
90 | } as const
91 |
92 | export type StatusBarStyle =
93 | (typeof StatusBarStyle)[keyof typeof StatusBarStyle]
94 |
95 | export const UserInterfaceStyle = {
96 | Unspecified: 'unspecified',
97 | Light: 'light',
98 | Dark: 'dark',
99 | } as const
100 |
101 | export type UserInterfaceStyle =
102 | (typeof UserInterfaceStyle)[keyof typeof UserInterfaceStyle]
103 |
104 | /**
105 | * Compact description of a color palette for light/dark/high-contrast modes.
106 | * When provided, native layers pick the most appropriate value per platform.
107 | */
108 | export interface DynamicColor {
109 | /** Primary color used regardless of theme (fallback). */
110 | base?: string
111 | /** Primary color used for light interfaces. */
112 | light?: string
113 | /** Primary color used for dark interfaces. */
114 | dark?: string
115 | /** High contrast override applied when available (iOS 26+, Android 16+). */
116 | highContrast?: string
117 | }
118 |
119 | /**
120 | * Reader mode result sizing used when presenting as a form sheet.
121 | */
122 | export interface FormSheetContentSize {
123 | width: number
124 | height: number
125 | }
126 |
127 | /**
128 | * iOS specific presentation and styling options.
129 | */
130 | export interface InAppBrowserIOSOptions {
131 | dismissButtonStyle?: DismissButtonStyle
132 | preferredBarTintColor?: DynamicColor
133 | preferredControlTintColor?: DynamicColor
134 | /**
135 | * Tint color applied to the status bar buttons when supported (iOS 15+).
136 | */
137 | preferredStatusBarStyle?: StatusBarStyle
138 | readerMode?: boolean
139 | animated?: boolean
140 | modalPresentationStyle?: ModalPresentationStyle
141 | modalTransitionStyle?: ModalTransitionStyle
142 | modalEnabled?: boolean
143 | enableBarCollapsing?: boolean
144 | ephemeralWebSession?: boolean
145 | enableEdgeDismiss?: boolean
146 | overrideUserInterfaceStyle?: UserInterfaceStyle
147 | formSheetPreferredContentSize?: FormSheetContentSize
148 | }
149 |
150 | /**
151 | * Android specific presentation and styling options.
152 | */
153 | export interface InAppBrowserAndroidOptions {
154 | showTitle?: boolean
155 | toolbarColor?: DynamicColor
156 | secondaryToolbarColor?: DynamicColor
157 | navigationBarColor?: DynamicColor
158 | navigationBarDividerColor?: DynamicColor
159 | enableUrlBarHiding?: boolean
160 | enableDefaultShare?: boolean
161 | shareState?: BrowserShareState
162 | colorScheme?: BrowserColorScheme
163 | headers?: Record
164 | forceCloseOnRedirection?: boolean
165 | hasBackButton?: boolean
166 | browserPackage?: string
167 | showInRecents?: boolean
168 | includeReferrer?: boolean
169 | instantAppsEnabled?: boolean
170 | enablePullToRefresh?: boolean
171 | enablePartialCustomTab?: boolean
172 | animations?: BrowserAnimations
173 | }
174 |
175 | /**
176 | * Declarative animation configuration for Android Custom Tabs.
177 | */
178 | export interface BrowserAnimations {
179 | startEnter?: string
180 | startExit?: string
181 | endEnter?: string
182 | endExit?: string
183 | }
184 |
185 | /**
186 | * Aggregated cross-platform options.
187 | */
188 | export interface InAppBrowserOptions
189 | extends InAppBrowserIOSOptions,
190 | InAppBrowserAndroidOptions {
191 | headers?: Record
192 | }
193 |
194 | /**
195 | * Result payload returned by imperative API calls.
196 | */
197 | export interface InAppBrowserResult {
198 | type: BrowserResultType
199 | url?: string
200 | message?: string
201 | }
202 |
203 | /**
204 | * Authentication result payload (mirrors regular result semantics).
205 | */
206 | export interface InAppBrowserAuthResult extends InAppBrowserResult {}
207 |
208 | export interface InappbrowserNitro
209 | extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
210 | /**
211 | * Report whether the Native runtime can present an in-app browser.
212 | */
213 | isAvailable(): Promise
214 |
215 | /**
216 | * Present an in-app browser with the supplied configuration.
217 | */
218 | open(url: string, options?: InAppBrowserOptions): Promise
219 |
220 | /**
221 | * Launch an authentication flow and resolve with the redirect payload.
222 | */
223 | openAuth(
224 | url: string,
225 | redirectUrl: string,
226 | options?: InAppBrowserOptions
227 | ): Promise
228 |
229 | /**
230 | * Close any visible browser session.
231 | */
232 | close(): Promise
233 |
234 | /**
235 | * Dismiss an ongoing authentication session.
236 | */
237 | closeAuth(): Promise
238 |
239 | }
--------------------------------------------------------------------------------
/ios/SafariPresenter.swift:
--------------------------------------------------------------------------------
1 | import SafariServices
2 | import UIKit
3 |
4 | final class SafariPresenter: NSObject {
5 | private var controller: NitroSafariViewController?
6 | private var delegate: SafariDismissDelegate?
7 |
8 | @MainActor
9 | func present(urlString: String, options: InAppBrowserOptions?) -> InAppBrowserResult {
10 | guard let url = URL(string: urlString) else {
11 | return InAppBrowserResult(type: .dismiss, url: nil, message: "invalid url")
12 | }
13 |
14 | let configuration = SFSafariViewController.Configuration()
15 | configuration.entersReaderIfAvailable = options?.readerMode ?? false
16 | configuration.barCollapsingEnabled = options?.enableBarCollapsing ?? false
17 |
18 | let controller = NitroSafariViewController(
19 | url: url,
20 | configuration: configuration,
21 | statusBarStyle: SafariStyleMapper.statusBarStyle(from: options?.preferredStatusBarStyle),
22 | userInterfaceStyle: SafariStyleMapper.interfaceStyle(from: options?.overrideUserInterfaceStyle)
23 | )
24 |
25 | apply(options: options, to: controller)
26 |
27 | let delegate = SafariDismissDelegate { [weak self] in
28 | self?.controller = nil
29 | self?.delegate = nil
30 | }
31 |
32 | controller.delegate = delegate
33 | controller.isModalInPresentation = !(options?.enableEdgeDismiss ?? true)
34 |
35 | guard let presenter = UIApplication.shared.nitroTopMostViewController else {
36 | return InAppBrowserResult(type: .dismiss, url: nil, message: "missing presenter")
37 | }
38 |
39 | self.controller = controller
40 | self.delegate = delegate
41 |
42 | let animated = options?.animated ?? true
43 |
44 | if options?.modalEnabled == false,
45 | let navigation = (presenter as? UINavigationController) ?? presenter.navigationController {
46 | navigation.pushViewController(controller, animated: animated)
47 | } else {
48 | presenter.present(controller, animated: animated)
49 | }
50 |
51 | return InAppBrowserResult(type: .success, url: url.absoluteString, message: nil)
52 | }
53 |
54 | @MainActor
55 | func dismiss() async {
56 | guard let controller else { return }
57 |
58 | if let navigation = controller.navigationController,
59 | navigation.viewControllers.contains(where: { $0 === controller }) {
60 | if navigation.topViewController === controller {
61 | navigation.popViewController(animated: true)
62 | } else {
63 | navigation.viewControllers.removeAll { $0 === controller }
64 | }
65 | } else if controller.presentingViewController != nil {
66 | await withCheckedContinuation { continuation in
67 | controller.dismiss(animated: true) {
68 | continuation.resume(returning: ())
69 | }
70 | }
71 | }
72 |
73 | self.controller = nil
74 | self.delegate = nil
75 | }
76 |
77 | private func apply(options: InAppBrowserOptions?, to controller: SFSafariViewController) {
78 | if let barColor = UIColor.from(dynamicColor: options?.preferredBarTintColor) {
79 | controller.preferredBarTintColor = barColor
80 | }
81 |
82 | if let controlColor = UIColor.from(dynamicColor: options?.preferredControlTintColor) {
83 | controller.preferredControlTintColor = controlColor
84 | }
85 |
86 | if let dismissStyle = options?.dismissButtonStyle {
87 | controller.dismissButtonStyle = SafariStyleMapper.dismissButtonStyle(from: dismissStyle)
88 | }
89 |
90 | if let formSize = options?.formSheetPreferredContentSize {
91 | controller.preferredContentSize = CGSize(width: CGFloat(formSize.width), height: CGFloat(formSize.height))
92 | }
93 |
94 | if let presentation = options?.modalPresentationStyle {
95 | controller.modalPresentationStyle = SafariStyleMapper.presentationStyle(from: presentation)
96 | }
97 |
98 | if let transition = options?.modalTransitionStyle {
99 | controller.modalTransitionStyle = SafariStyleMapper.transitionStyle(from: transition)
100 | if transition == .partialcurl {
101 | controller.modalPresentationStyle = .fullScreen
102 | }
103 | }
104 | }
105 | }
106 |
107 | private final class NitroSafariViewController: SFSafariViewController {
108 | private let resolvedStatusBarStyle: UIStatusBarStyle?
109 |
110 | init(url: URL, configuration: SFSafariViewController.Configuration, statusBarStyle: UIStatusBarStyle?, userInterfaceStyle: UIUserInterfaceStyle?) {
111 | resolvedStatusBarStyle = statusBarStyle
112 | super.init(url: url, configuration: configuration)
113 |
114 | if let userInterfaceStyle, #available(iOS 13.0, *) {
115 | overrideUserInterfaceStyle = userInterfaceStyle
116 | }
117 | }
118 |
119 | override var preferredStatusBarStyle: UIStatusBarStyle {
120 | resolvedStatusBarStyle ?? super.preferredStatusBarStyle
121 | }
122 | }
123 |
124 | private final class SafariDismissDelegate: NSObject, SFSafariViewControllerDelegate {
125 | private let onDismiss: () -> Void
126 |
127 | init(onDismiss: @escaping () -> Void) {
128 | self.onDismiss = onDismiss
129 | }
130 |
131 | func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
132 | // iOS Simulator (and hardware without biometric setup) dismisses immediately for some auth flows.
133 | onDismiss()
134 | }
135 | }
136 |
137 | private enum SafariStyleMapper {
138 | static func dismissButtonStyle(from style: DismissButtonStyle) -> SFSafariViewController.DismissButtonStyle {
139 | switch style {
140 | case .cancel:
141 | return .cancel
142 | case .done:
143 | return .done
144 | case .close:
145 | return .close
146 | @unknown default:
147 | return .done
148 | }
149 | }
150 |
151 | static func presentationStyle(from style: ModalPresentationStyle) -> UIModalPresentationStyle {
152 | switch style {
153 | case .automatic:
154 | if #available(iOS 13.0, *) {
155 | return .automatic
156 | }
157 | return .fullScreen
158 | case .none:
159 | return .none
160 | case .fullscreen:
161 | return .fullScreen
162 | case .pagesheet:
163 | if #available(iOS 13.0, *) {
164 | return .pageSheet
165 | }
166 | return .formSheet
167 | case .formsheet:
168 | return .formSheet
169 | case .currentcontext:
170 | return .currentContext
171 | case .custom:
172 | return .custom
173 | case .overfullscreen:
174 | return .overFullScreen
175 | case .overcurrentcontext:
176 | return .overCurrentContext
177 | case .popover:
178 | return .popover
179 | @unknown default:
180 | return .automatic
181 | }
182 | }
183 |
184 | static func transitionStyle(from style: ModalTransitionStyle) -> UIModalTransitionStyle {
185 | switch style {
186 | case .coververtical:
187 | return .coverVertical
188 | case .fliphorizontal:
189 | return .flipHorizontal
190 | case .crossdissolve:
191 | return .crossDissolve
192 | case .partialcurl:
193 | return .partialCurl
194 | @unknown default:
195 | return .coverVertical
196 | }
197 | }
198 |
199 | static func statusBarStyle(from style: StatusBarStyle?) -> UIStatusBarStyle? {
200 | guard let style else { return nil }
201 | switch style {
202 | case .default:
203 | return .default
204 | case .lightcontent:
205 | return .lightContent
206 | case .darkcontent:
207 | if #available(iOS 13.0, *) {
208 | return .darkContent
209 | }
210 | return .default
211 | @unknown default:
212 | return nil
213 | }
214 | }
215 |
216 | static func interfaceStyle(from style: UserInterfaceStyle?) -> UIUserInterfaceStyle? {
217 | guard let style, #available(iOS 13.0, *) else { return nil }
218 | switch style {
219 | case .unspecified:
220 | return .unspecified
221 | case .light:
222 | return .light
223 | case .dark:
224 | return .dark
225 | @unknown default:
226 | return nil
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/example/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/android/src/main/java/com/inappbrowsernitro/browser/CustomTabsIntentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.inappbrowsernitro.browser
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.graphics.Bitmap
6 | import android.graphics.Canvas
7 | import android.graphics.Color
8 | import android.graphics.Paint
9 | import android.graphics.Path
10 | import android.net.Uri
11 | import android.os.Build
12 | import android.os.Bundle
13 | import androidx.browser.customtabs.CustomTabColorSchemeParams
14 | import androidx.browser.customtabs.CustomTabsIntent
15 | import androidx.browser.customtabs.CustomTabsSession
16 | import com.margelo.nitro.inappbrowsernitro.BrowserAnimations
17 | import com.margelo.nitro.inappbrowsernitro.BrowserColorScheme
18 | import com.margelo.nitro.inappbrowsernitro.BrowserShareState
19 | import com.margelo.nitro.inappbrowsernitro.InAppBrowserOptions
20 |
21 | internal class CustomTabsIntentFactory(
22 | private val context: Context,
23 | private val session: CustomTabsSession?
24 | ) {
25 | fun create(options: InAppBrowserOptions?): CustomTabsIntent {
26 | val builder = session?.let { CustomTabsIntent.Builder(it) } ?: CustomTabsIntent.Builder()
27 |
28 | applyColors(builder, options)
29 | applyBehaviours(builder, options)
30 | applyNavigation(builder, options)
31 | applyAnimations(builder, options?.animations)
32 |
33 | val intent = builder.build()
34 |
35 | configureIntent(intent.intent, options)
36 |
37 | options?.browserPackage?.takeIf { it.isNotBlank() }?.let(intent.intent::setPackage)
38 |
39 | return intent
40 | }
41 |
42 | private fun applyColors(builder: CustomTabsIntent.Builder, options: InAppBrowserOptions?) {
43 | val colorSchemes = buildColorParams(options)
44 |
45 | when {
46 | colorSchemes?.system != null -> builder.setDefaultColorSchemeParams(colorSchemes.system)
47 | colorSchemes?.light != null -> builder.setDefaultColorSchemeParams(colorSchemes.light)
48 | colorSchemes?.dark != null -> builder.setDefaultColorSchemeParams(colorSchemes.dark)
49 | }
50 |
51 | colorSchemes?.light?.let {
52 | builder.setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, it)
53 | }
54 |
55 | colorSchemes?.dark?.let {
56 | builder.setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, it)
57 | }
58 |
59 | options?.colorScheme?.let { scheme ->
60 | builder.setColorScheme(scheme.toCustomTabsScheme())
61 | }
62 | }
63 |
64 | private fun applyBehaviours(builder: CustomTabsIntent.Builder, options: InAppBrowserOptions?) {
65 | builder.setShowTitle(options?.showTitle ?: true)
66 |
67 | options?.enableUrlBarHiding?.let(builder::setUrlBarHidingEnabled)
68 |
69 | when (options?.shareState) {
70 | BrowserShareState.ON -> builder.setShareState(CustomTabsIntent.SHARE_STATE_ON)
71 | BrowserShareState.OFF -> builder.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
72 | BrowserShareState.DEFAULT, null -> if (options?.enableDefaultShare == false) {
73 | builder.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
74 | }
75 | }
76 |
77 | options?.instantAppsEnabled?.let(builder::setInstantAppsEnabled)
78 |
79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && options?.enablePartialCustomTab == true) {
80 | val height = (context.resources.displayMetrics.heightPixels * PARTIAL_TAB_RATIO).toInt()
81 | builder.setInitialActivityHeightPx(height, CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE)
82 | }
83 |
84 | // Emulators rely on soft navigation buttons; keeping pull-to-refresh disabled avoids accidental reloads.
85 | if (options?.enablePullToRefresh == true) {
86 | builder.setUrlBarHidingEnabled(true)
87 | }
88 | }
89 |
90 | private fun applyNavigation(builder: CustomTabsIntent.Builder, options: InAppBrowserOptions?) {
91 | if (options?.hasBackButton == true) {
92 | builder.setCloseButtonIcon(createBackArrow())
93 | }
94 | }
95 |
96 | private fun applyAnimations(builder: CustomTabsIntent.Builder, animations: BrowserAnimations?) {
97 | animations ?: return
98 | val startEnter = resolveAnimation(animations.startEnter)
99 | val startExit = resolveAnimation(animations.startExit)
100 | if (startEnter != null && startExit != null) {
101 | builder.setStartAnimations(context, startEnter, startExit)
102 | }
103 |
104 | val endEnter = resolveAnimation(animations.endEnter)
105 | val endExit = resolveAnimation(animations.endExit)
106 | if (endEnter != null && endExit != null) {
107 | builder.setExitAnimations(context, endEnter, endExit)
108 | }
109 | }
110 |
111 | private fun configureIntent(intent: Intent, options: InAppBrowserOptions?) {
112 | if (options?.showInRecents == false) {
113 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
114 | }
115 |
116 | if (options?.includeReferrer == true) {
117 | val referrer = Uri.parse("android-app://${context.packageName}")
118 | intent.putExtra(Intent.EXTRA_REFERRER, referrer)
119 | }
120 |
121 | options?.headers?.takeIf { it.isNotEmpty() }?.let { headers ->
122 | val bundle = Bundle()
123 | headers.forEach { (key, value) ->
124 | bundle.putString(key, value)
125 | }
126 | intent.putExtra(BROWSER_EXTRA_HEADERS, bundle)
127 | }
128 | }
129 |
130 | private fun buildColorParams(options: InAppBrowserOptions?): ColorSchemeParams? {
131 | val system = resolveColorSet(options, DynamicColorResolver.DynamicScheme.SYSTEM)
132 | val light = resolveColorSet(options, DynamicColorResolver.DynamicScheme.LIGHT)
133 | val dark = resolveColorSet(options, DynamicColorResolver.DynamicScheme.DARK)
134 |
135 | val systemParams = system.toCustomTabParams()
136 | val lightParams = light.toCustomTabParams()
137 | val darkParams = dark.toCustomTabParams()
138 |
139 | if (systemParams == null && lightParams == null && darkParams == null) {
140 | return null
141 | }
142 |
143 | return ColorSchemeParams(
144 | system = systemParams,
145 | light = lightParams,
146 | dark = darkParams,
147 | )
148 | }
149 |
150 | private fun resolveColorSet(options: InAppBrowserOptions?, scheme: DynamicColorResolver.DynamicScheme): ColorSet {
151 | val toolbar = DynamicColorResolver.resolveForScheme(options?.toolbarColor, scheme)
152 | val secondaryToolbar = DynamicColorResolver.resolveForScheme(options?.secondaryToolbarColor, scheme)
153 | val navigationBar = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
154 | DynamicColorResolver.resolveForScheme(options?.navigationBarColor, scheme)
155 | } else {
156 | null
157 | }
158 | val navigationDivider = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
159 | DynamicColorResolver.resolveForScheme(options?.navigationBarDividerColor, scheme)
160 | } else {
161 | null
162 | }
163 |
164 | return ColorSet(
165 | toolbar = toolbar,
166 | secondaryToolbar = secondaryToolbar,
167 | navigationBar = navigationBar,
168 | navigationBarDivider = navigationDivider,
169 | )
170 | }
171 |
172 | private fun resolveAnimation(name: String?): Int? {
173 | if (name.isNullOrBlank()) {
174 | return null
175 | }
176 |
177 | val identifier = context.resources.getIdentifier(name, "anim", context.packageName)
178 | return identifier.takeIf { it != 0 }
179 | }
180 |
181 | private fun createBackArrow(): Bitmap {
182 | val size = context.resources.displayMetrics.density * 24
183 | val bitmap = Bitmap.createBitmap(size.toInt(), size.toInt(), Bitmap.Config.ARGB_8888)
184 | val canvas = Canvas(bitmap)
185 |
186 | val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
187 | color = DEFAULT_CLOSE_BUTTON_COLOR
188 | style = Paint.Style.STROKE
189 | strokeWidth = context.resources.displayMetrics.density * 2
190 | strokeCap = Paint.Cap.ROUND
191 | strokeJoin = Paint.Join.ROUND
192 | }
193 |
194 | val path = Path().apply {
195 | moveTo(size * 0.75f, size * 0.2f)
196 | lineTo(size * 0.35f, size * 0.5f)
197 | lineTo(size * 0.75f, size * 0.8f)
198 | }
199 |
200 | canvas.drawPath(path, paint)
201 | return bitmap
202 | }
203 |
204 | private data class ColorSchemeParams(
205 | val system: CustomTabColorSchemeParams?,
206 | val light: CustomTabColorSchemeParams?,
207 | val dark: CustomTabColorSchemeParams?,
208 | )
209 |
210 | private data class ColorSet(
211 | val toolbar: Int?,
212 | val secondaryToolbar: Int?,
213 | val navigationBar: Int?,
214 | val navigationBarDivider: Int?,
215 | ) {
216 | fun hasAny(): Boolean {
217 | return toolbar != null || secondaryToolbar != null || navigationBar != null || navigationBarDivider != null
218 | }
219 |
220 | fun toCustomTabParams(): CustomTabColorSchemeParams? {
221 | if (!hasAny()) {
222 | return null
223 | }
224 |
225 | return CustomTabColorSchemeParams.Builder().apply {
226 | toolbar?.let(::setToolbarColor)
227 | secondaryToolbar?.let(::setSecondaryToolbarColor)
228 | navigationBar?.let(::setNavigationBarColor)
229 | navigationBarDivider?.let(::setNavigationBarDividerColor)
230 | }.build()
231 | }
232 | }
233 |
234 | private fun BrowserColorScheme.toCustomTabsScheme(): Int {
235 | return when (this) {
236 | BrowserColorScheme.LIGHT -> CustomTabsIntent.COLOR_SCHEME_LIGHT
237 | BrowserColorScheme.DARK -> CustomTabsIntent.COLOR_SCHEME_DARK
238 | BrowserColorScheme.SYSTEM -> CustomTabsIntent.COLOR_SCHEME_SYSTEM
239 | }
240 | }
241 |
242 | private companion object {
243 | private const val PARTIAL_TAB_RATIO = 0.85f
244 | private const val BROWSER_EXTRA_HEADERS = "android.support.customtabs.extra.EXTRA_HEADERS"
245 | private val DEFAULT_CLOSE_BUTTON_COLOR = Color.argb(0xFF, 0x3A, 0x3A, 0x3A)
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'
2 | import {
3 | Platform,
4 | SafeAreaView,
5 | ScrollView,
6 | StyleSheet,
7 | Text,
8 | TouchableOpacity,
9 | View,
10 | } from 'react-native'
11 | import {
12 | InAppBrowser,
13 | useInAppBrowser,
14 | BrowserShareState,
15 | } from 'react-native-inappbrowser-nitro'
16 |
17 | import type { InAppBrowserResult } from 'react-native-inappbrowser-nitro'
18 |
19 | const REPO_URL = 'https://github.com/mCodex/react-native-inappbrowser-nitro'
20 | const AUTH_REDIRECT_URL = 'inappbrowsernitro://callback'
21 | const AUTH_URL = `https://httpbin.org/redirect-to?url=${encodeURIComponent(AUTH_REDIRECT_URL)}`
22 |
23 | const toolbarPalette = {
24 | base: '#2563EB',
25 | dark: '#1E3A8A',
26 | highContrast: '#1D4ED8',
27 | }
28 |
29 | const controlPalette = {
30 | base: '#FFFFFF',
31 | highContrast: '#FFD700',
32 | }
33 |
34 | const ios26ShowcasePalette = {
35 | base: '#F97316',
36 | light: '#FDBA74',
37 | dark: '#7C3AED',
38 | highContrast: '#DC2626',
39 | }
40 |
41 | const ios26ControlPalette = {
42 | base: '#0F172A',
43 | light: '#1E293B',
44 | dark: '#F8FAFC',
45 | highContrast: '#0EA5E9',
46 | }
47 |
48 | type ExampleButtonProps = {
49 | label: string
50 | onPress: () => Promise | void
51 | disabled?: boolean
52 | tone?: 'primary' | 'secondary'
53 | }
54 |
55 | const ExampleButton = ({ label, onPress, disabled, tone = 'primary' }: ExampleButtonProps) => {
56 | const backgroundStyle = useMemo(() => {
57 | if (disabled) {
58 | return styles.buttonDisabled
59 | }
60 | return tone === 'primary' ? styles.buttonPrimary : styles.buttonSecondary
61 | }, [disabled, tone])
62 |
63 | return (
64 |
65 | {label}
66 |
67 | )
68 | }
69 |
70 | const formatResult = (result: InAppBrowserResult) => {
71 | const parts = [`type: ${result.type}`]
72 | if (result.url) {
73 | parts.push(`url: ${result.url}`)
74 | }
75 | if (result.message) {
76 | parts.push(`message: ${result.message}`)
77 | }
78 | return parts.join(' • ')
79 | }
80 |
81 | const formatError = (err: unknown) => {
82 | if (err instanceof Error) {
83 | return `error: ${err.message}`
84 | }
85 | return `error: ${String(err)}`
86 | }
87 |
88 | function App(): React.JSX.Element {
89 | const { open, openAuth, close, isLoading, error } = useInAppBrowser()
90 | const [isSupported, setIsSupported] = useState(null)
91 | const [log, setLog] = useState([])
92 |
93 | useEffect(() => {
94 | InAppBrowser.isAvailable()
95 | .then(setIsSupported)
96 | .catch(() => setIsSupported(false))
97 | }, [])
98 |
99 | const pushLog = useCallback((message: string) => {
100 | setLog(current => [message, ...current].slice(0, 6))
101 | }, [])
102 |
103 | const handleOpenDocs = useCallback(async () => {
104 | try {
105 | const result = await open(REPO_URL, {
106 | preferredBarTintColor: toolbarPalette,
107 | preferredControlTintColor: controlPalette,
108 | preferredStatusBarStyle: 'lightContent',
109 | overrideUserInterfaceStyle: 'dark',
110 | toolbarColor: toolbarPalette,
111 | secondaryToolbarColor: { base: '#111827' },
112 | navigationBarColor: { base: '#111827' },
113 | enablePartialCustomTab: true,
114 | enablePullToRefresh: true,
115 | includeReferrer: true,
116 | shareState: BrowserShareState.Off,
117 | })
118 |
119 | pushLog(formatResult(result))
120 | } catch (err) {
121 | pushLog(formatError(err))
122 | }
123 | }, [open, pushLog])
124 |
125 | const handleOpenReader = useCallback(async () => {
126 | try {
127 | const result = await open(REPO_URL, {
128 | readerMode: true,
129 | enableBarCollapsing: true,
130 | dismissButtonStyle: 'close',
131 | preferredBarTintColor: { base: '#FFFFFF', dark: '#111827' },
132 | preferredControlTintColor: controlPalette,
133 | enableEdgeDismiss: true,
134 | })
135 |
136 | pushLog(formatResult(result))
137 | } catch (err) {
138 | pushLog(formatError(err))
139 | }
140 | }, [open, pushLog])
141 |
142 | const handleOpenIos26Palette = useCallback(async () => {
143 | try {
144 | const result = await open(REPO_URL, {
145 | preferredBarTintColor: ios26ShowcasePalette,
146 | preferredControlTintColor: ios26ControlPalette,
147 | preferredStatusBarStyle: 'darkContent',
148 | overrideUserInterfaceStyle: 'light',
149 | dismissButtonStyle: 'done',
150 | enableEdgeDismiss: false,
151 | formSheetPreferredContentSize: { width: 414, height: 720 },
152 | })
153 |
154 | pushLog(`iOS 26 palette • ${formatResult(result)}`)
155 | } catch (err) {
156 | pushLog(formatError(err))
157 | }
158 | }, [open, pushLog])
159 |
160 | const handleAuth = useCallback(async () => {
161 | try {
162 | const result = await openAuth(AUTH_URL, AUTH_REDIRECT_URL, {
163 | ephemeralWebSession: true,
164 | enableEdgeDismiss: false,
165 | preferredBarTintColor: toolbarPalette,
166 | toolbarColor: toolbarPalette,
167 | includeReferrer: true,
168 | })
169 |
170 | pushLog(formatResult(result))
171 | } catch (err) {
172 | pushLog(formatError(err))
173 | }
174 | }, [openAuth, pushLog])
175 |
176 | const handleClose = useCallback(async () => {
177 | try {
178 | await close()
179 | pushLog('close(): requested dismissal')
180 | } catch (err) {
181 | pushLog(formatError(err))
182 | }
183 | }, [close, pushLog])
184 |
185 | const supportCopy = useMemo(() => {
186 | if (isSupported === null) {
187 | return 'Checking native availability…'
188 | }
189 | if (!isSupported) {
190 | return 'Native browser support is unavailable on this device/emulator.'
191 | }
192 | return 'Native browser support detected.'
193 | }, [isSupported])
194 |
195 | return (
196 |
197 |
198 | react-native-inappbrowser-nitro
199 | Nitro-powered in-app browser with iOS 26 & Android 16 features.
200 |
201 |
202 | Status
203 | {supportCopy}
204 | Loading: {isLoading ? 'yes' : 'no'}
205 | {error && Last error: {error.message}}
206 |
207 |
208 |
209 | Try the Features
210 |
211 |
217 |
222 |
227 |
228 |
229 |
230 |
231 | Security Notes
232 |
233 | iOS 26 allows blocking swipe-to-dismiss during sensitive auth flows via enableEdgeDismiss. Android
234 | 16 adds dynamic contrast colors and partial custom tabs; emulators without gesture navigation may require the hardware back
235 | button to exit.
236 |
237 |
238 | The showcase button demonstrates the new iOS 26 palette controls, including high-contrast overrides, status bar tinting, and
239 | form-sheet sizing. On older iOS versions the system gracefully ignores unsupported keys.
240 |
241 |
242 |
243 |
244 | Recent Results
245 | {log.length === 0 ? (
246 | Interact with the buttons above to populate the log.
247 | ) : (
248 | log.map((entry, index) => (
249 |
250 | {index + 1}. {entry}
251 |
252 | ))
253 | )}
254 |
255 |
256 |
257 |
258 | Update AUTH_URL in example/App.tsx with your provider's authorize endpoint and
259 | register {AUTH_REDIRECT_URL} in your native projects to test redirect-based flows.
260 |
261 |
262 |
263 |
264 | )
265 | }
266 |
267 | const styles = StyleSheet.create({
268 | safeArea: {
269 | flex: 1,
270 | backgroundColor: '#0F172A',
271 | },
272 | content: {
273 | padding: 24,
274 | gap: 24,
275 | },
276 | title: {
277 | fontSize: 24,
278 | fontWeight: '700',
279 | color: '#F8FAFC',
280 | },
281 | subtitle: {
282 | fontSize: 14,
283 | color: '#CBD5F5',
284 | },
285 | card: {
286 | backgroundColor: '#111827',
287 | borderRadius: 12,
288 | padding: 16,
289 | gap: 12,
290 | },
291 | sectionTitle: {
292 | fontSize: 16,
293 | fontWeight: '600',
294 | color: '#F8FAFC',
295 | },
296 | paragraph: {
297 | fontSize: 14,
298 | color: '#E2E8F0',
299 | lineHeight: 20,
300 | },
301 | errorText: {
302 | color: '#F87171',
303 | },
304 | button: {
305 | paddingVertical: 12,
306 | borderRadius: 10,
307 | alignItems: 'center',
308 | },
309 | buttonPrimary: {
310 | backgroundColor: '#2563EB',
311 | },
312 | buttonSecondary: {
313 | backgroundColor: '#334155',
314 | },
315 | buttonDisabled: {
316 | backgroundColor: '#1E293B',
317 | opacity: 0.6,
318 | },
319 | buttonText: {
320 | color: '#F8FAFC',
321 | fontSize: 15,
322 | fontWeight: '600',
323 | },
324 | logLine: {
325 | fontSize: 13,
326 | color: '#94A3B8',
327 | },
328 | footer: {
329 | paddingBottom: 32,
330 | },
331 | footerText: {
332 | fontSize: 13,
333 | color: '#CBD5F5',
334 | lineHeight: 18,
335 | },
336 | code: {
337 | fontFamily: 'Courier',
338 | color: '#38BDF8',
339 | },
340 | })
341 |
342 | export default App
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-inappbrowser-nitro
2 |
3 | [](https://www.npmjs.com/package/react-native-inappbrowser-nitro)
4 | [](https://www.npmjs.com/package/react-native-inappbrowser-nitro)
5 | [](LICENSE)
6 |
7 | Lightning-fast, modern in-app browser for React Native powered by Nitro Modules. Enjoy direct JSI bindings, zero bridge overhead, and polished native browser UX on both iOS and Android.
8 |
9 |
10 |

11 |
12 |
13 | ## Contents
14 |
15 | - [Highlights](#highlights)
16 | - [Platform Support](#platform-support)
17 | - [Installation](#installation)
18 | - [Usage](#usage)
19 | - [Migration Guide (pre-Nitro → latest)](#migration-guide-pre-nitro--latest)
20 | - [API Surface](#api-surface)
21 | - [Options Reference](#options-reference)
22 | - [Dynamic Color Palettes](#dynamic-color-palettes)
23 | - [Security & Emulator Notes](#security--emulator-notes)
24 | - [Troubleshooting](#troubleshooting)
25 | - [Contributing](#contributing)
26 | - [License](#license)
27 |
28 | ## Highlights
29 |
30 | - TurboModule-first implementation with native-speed execution and zero bridge overhead.
31 | - Fully typed TypeScript API plus ergonomic React hook helpers.
32 | - ✅ **iOS 26 ready**: high-contrast dynamic colors, edge-dismiss control, and UI style overrides.
33 | - ✅ **Android 16 ready**: partial custom tabs, referrer controls, and per-theme palettes.
34 | - Authentication-first design with ephemeral sessions and graceful fallbacks.
35 | - Works great with Hermes, Fabric, and the React Native New Architecture.
36 |
37 | ## Platform Support
38 |
39 | | Platform | Minimum | Targeted | Highlights |
40 | |----------|---------|----------|------------|
41 | | iOS | 11.0 | 17 / 26* | SafariViewController + ASWebAuthentication support. |
42 | | Android | API 23 | API 34 / 16* | Chrome Custom Tabs with dynamic theming. |
43 | | React Native | 0.70.0 | Latest | Nitro Modules + TypeScript bindings. |
44 |
45 | \* iOS 26 and Android 16 features are automatically gated behind runtime checks.
46 |
47 | > **Expo** is not supported because Nitro modules require native compilation.
48 |
49 | ## Android Browser Fallback
50 |
51 |
52 | Browser selection logic on Android
53 |
54 | On Android, the library prioritizes Chrome Custom Tabs for the best user experience when Chrome is installed. If Chrome is not available (e.g., on Samsung devices), it falls back to opening the URL in the device's default web browser using the standard `Intent.ACTION_VIEW` mechanism.
55 |
56 | This ensures compatibility across devices while maintaining optimal performance with Chrome when possible.
57 |
58 |
59 | ## Installation
60 |
61 | ```sh
62 | npm install react-native-inappbrowser-nitro react-native-nitro-modules
63 | ```
64 |
65 | or
66 |
67 | ```sh
68 | yarn add react-native-inappbrowser-nitro react-native-nitro-modules
69 | ```
70 |
71 | ### iOS
72 |
73 | ```sh
74 | cd ios
75 | pod install
76 | ```
77 |
78 | ### Android
79 |
80 | No additional steps—Gradle autolinking handles everything.
81 |
82 | ## Usage
83 |
84 | ### Imperative API
85 |
86 | ```tsx
87 | import { InAppBrowser } from 'react-native-inappbrowser-nitro'
88 |
89 | async function openDocs() {
90 | if (!(await InAppBrowser.isAvailable())) {
91 | console.warn('No compatible browser found')
92 | return
93 | }
94 |
95 | const result = await InAppBrowser.open('https://nitro.margelo.com', {
96 | preferredBarTintColor: { base: '#111827', light: '#1F2933', highContrast: '#000000' },
97 | preferredControlTintColor: { base: '#F9FAFB', highContrast: '#FFD700' }, // iOS 26+
98 | toolbarColor: { base: '#2563EB', dark: '#1E3A8A' },
99 | enablePartialCustomTab: true, // Android 16+
100 | })
101 |
102 | console.log(result)
103 | }
104 | ```
105 |
106 | ### React Hook
107 |
108 | ```tsx
109 | import { useInAppBrowser } from 'react-native-inappbrowser-nitro'
110 |
111 | export function LaunchButton() {
112 | const { open, isLoading, error } = useInAppBrowser()
113 |
114 | return (
115 |