Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Specify Parameter](#specify-parameter)
- [Specify a Custom Authorize URL](#specify-a-custom-authorize-url)
- [Customize the Custom Tabs UI](#customize-the-custom-tabs-ui)
- [Partial Custom Tabs (Bottom Sheet and Side Sheet)](#partial-custom-tabs-bottom-sheet-and-side-sheet)
- [Changing the Return To URL scheme](#changing-the-return-to-url-scheme)
- [Specify a Custom Logout URL](#specify-a-custom-logout-url)
- [Trusted Web Activity](#trusted-web-activity)
Expand Down Expand Up @@ -175,6 +176,75 @@ WebAuthProvider.login(account)
```
</details>

### Partial Custom Tabs (Bottom Sheet and Side Sheet)

You can present the authentication flow as a **bottom sheet** on compact screens or a **side sheet** on larger screens (e.g., tablets and foldables) instead of a full-screen browser tab. This is configured through `CustomTabsOptions`.

> **Browser compatibility:**
> - **Bottom sheet** (Partial Custom Tabs) requires **Chrome 107+** (or another Custom Tabs browser that supports the Partial Custom Tabs protocol).
> - **Side sheet** requires **Chrome 120+** (or another browser that supports side-sheet Custom Tabs).
>
> If the user's browser does not meet the minimum version requirement, the authentication flow automatically falls back to a standard full-screen Custom Tab (or a full-screen browser tab if Custom Tabs are unsupported). It is therefore safe to enable these options unconditionally — users on older browsers will simply see the full-screen experience.

#### Bottom sheet

```kotlin
val ctOptions = CustomTabsOptions.newBuilder()
.withInitialHeight(700) // initial height in dp
.withResizable(true) // allow the user to drag to resize (default)
.withToolbarCornerRadius(16) // rounded top corners (0–16 dp)
.build()

WebAuthProvider.login(account)
.withCustomTabsOptions(ctOptions)
.start(this, callback)
```

#### Side sheet (with bottom-sheet fallback on narrow screens)

```kotlin
val ctOptions = CustomTabsOptions.newBuilder()
.withInitialHeight(700) // used when the screen is narrower than the breakpoint
.withInitialWidth(500) // initial side-sheet width in dp
.withSideSheetBreakpoint(840) // screens wider than this render as a side sheet
.build()

WebAuthProvider.login(account)
.withCustomTabsOptions(ctOptions)
.start(this, callback)
```

If `withSideSheetBreakpoint` is not set, the browser's default breakpoint (typically 840 dp in Chrome) applies, so devices narrower than that will continue to render as a bottom sheet or full screen.

#### Allow interaction with the app behind the partial tab

By default, the app behind a Partial Custom Tab is non-interactive. Enable pass-through interaction with:

```kotlin
val ctOptions = CustomTabsOptions.newBuilder()
.withInitialHeight(700)
.withBackgroundInteractionEnabled(true)
.build()
```

<details>
<summary>Using Java</summary>

```java
CustomTabsOptions options = CustomTabsOptions.newBuilder()
.withInitialHeight(700)
.withInitialWidth(500)
.withSideSheetBreakpoint(840)
.withToolbarCornerRadius(16)
.withBackgroundInteractionEnabled(true)
.build();

WebAuthProvider.login(account)
.withCustomTabsOptions(options)
.start(MainActivity.this, callback);
```
</details>

## Changing the Return To URL scheme
This configuration will probably match what you've done for the [authentication setup](#a-note-about-app-deep-linking).

Expand Down
2 changes: 1 addition & 1 deletion auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.browser:browser:1.8.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.DisplayMetrics;

import androidx.annotation.ColorRes;
import androidx.annotation.Dimension;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
Expand All @@ -34,11 +36,33 @@ public class CustomTabsOptions implements Parcelable {
@Nullable
private final List<String> disabledCustomTabsPackages;

private CustomTabsOptions(boolean showTitle, @ColorRes int toolbarColor, @NonNull BrowserPicker browserPicker, @Nullable List<String> disabledCustomTabsPackages) {
// Partial Custom Tabs - Bottom Sheet
private final int initialHeight;
private final int activityHeightResizeBehavior;
private final int toolbarCornerRadius;

// Partial Custom Tabs - Side Sheet
private final int initialWidth;
private final int sideSheetBreakpoint;

// Partial Custom Tabs - Background Interaction
private final boolean backgroundInteractionEnabled;

private CustomTabsOptions(boolean showTitle, @ColorRes int toolbarColor, @NonNull BrowserPicker browserPicker,
@Nullable List<String> disabledCustomTabsPackages,
int initialHeight, int activityHeightResizeBehavior, int toolbarCornerRadius,
int initialWidth, int sideSheetBreakpoint,
boolean backgroundInteractionEnabled) {
this.showTitle = showTitle;
this.toolbarColor = toolbarColor;
this.browserPicker = browserPicker;
this.disabledCustomTabsPackages = disabledCustomTabsPackages;
this.initialHeight = initialHeight;
this.activityHeightResizeBehavior = activityHeightResizeBehavior;
this.toolbarCornerRadius = toolbarCornerRadius;
this.initialWidth = initialWidth;
this.sideSheetBreakpoint = sideSheetBreakpoint;
this.backgroundInteractionEnabled = backgroundInteractionEnabled;
}

@Nullable
Expand Down Expand Up @@ -88,6 +112,28 @@ Intent toIntent(@NonNull Context context, @Nullable CustomTabsSession session) {
.setToolbarColor(ContextCompat.getColor(context, toolbarColor));
builder.setDefaultColorSchemeParams(colorBuilder.build());
}

// Partial Custom Tabs - Bottom Sheet
Comment thread
subhankarmaiti marked this conversation as resolved.
if (initialHeight > 0) {
builder.setInitialActivityHeightPx(dpToPx(context, initialHeight), activityHeightResizeBehavior);
}
if (toolbarCornerRadius > 0) {
builder.setToolbarCornerRadiusDp(toolbarCornerRadius);
}

// Partial Custom Tabs - Side Sheet
if (initialWidth > 0) {
builder.setInitialActivityWidthPx(dpToPx(context, initialWidth));
}
if (sideSheetBreakpoint > 0) {
builder.setActivitySideSheetBreakpointDp(sideSheetBreakpoint);
}

// Partial Custom Tabs - Background Interaction
if (backgroundInteractionEnabled) {
builder.setBackgroundInteractionEnabled(true);
}

return builder.build().intent;
}

Expand All @@ -108,6 +154,12 @@ protected CustomTabsOptions(@NonNull Parcel in) {
toolbarColor = in.readInt();
browserPicker = in.readParcelable(BrowserPicker.class.getClassLoader());
disabledCustomTabsPackages = in.createStringArrayList();
initialHeight = in.readInt();
activityHeightResizeBehavior = in.readInt();
toolbarCornerRadius = in.readInt();
initialWidth = in.readInt();
sideSheetBreakpoint = in.readInt();
backgroundInteractionEnabled = in.readByte() != 0;
}

@Override
Expand All @@ -116,6 +168,12 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(toolbarColor);
dest.writeParcelable(browserPicker, flags);
dest.writeStringList(disabledCustomTabsPackages);
dest.writeInt(initialHeight);
dest.writeInt(activityHeightResizeBehavior);
dest.writeInt(toolbarCornerRadius);
dest.writeInt(initialWidth);
dest.writeInt(sideSheetBreakpoint);
dest.writeByte((byte) (backgroundInteractionEnabled ? 1 : 0));
}

@Override
Expand Down Expand Up @@ -147,11 +205,24 @@ public static class Builder {
@Nullable
private List<String> disabledCustomTabsPackages;

private int initialHeight;
private int activityHeightResizeBehavior;
private int toolbarCornerRadius;
private int initialWidth;
private int sideSheetBreakpoint;
private boolean backgroundInteractionEnabled;

Builder() {
this.showTitle = false;
this.toolbarColor = 0;
this.browserPicker = BrowserPicker.newBuilder().build();
this.disabledCustomTabsPackages = null;
this.initialHeight = 0;
this.activityHeightResizeBehavior = CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT;
this.toolbarCornerRadius = 0;
this.initialWidth = 0;
this.sideSheetBreakpoint = 0;
Comment thread
subhankarmaiti marked this conversation as resolved.
this.backgroundInteractionEnabled = false;
}

/**
Expand Down Expand Up @@ -212,15 +283,133 @@ public Builder withDisabledCustomTabsPackages(List<String> disabledCustomTabsPac
return this;
}

/**
* Sets the initial height for the Custom Tab to display as a bottom sheet.
* When set, the Custom Tab will appear as a bottom sheet instead of full screen.
* Pass the size in dp; it will be converted to pixels internally.
* The minimum height enforced by Chrome is 50% of the screen; values below this are auto-adjusted.
* Falls back to full screen on browsers that don't support Partial Custom Tabs (requires Chrome 107+).
* By default, the bottom sheet is resizable by the user. Use {@link #withResizable(boolean)}
* to lock the height.
*
* @param height the initial bottom sheet height in dp.
* @return this same builder instance.
*/
@NonNull
public Builder withInitialHeight(@Dimension(unit = Dimension.DP) int height) {
this.initialHeight = height;
return this;
}

/**
* Sets whether the user can resize the Partial Custom Tab by dragging.
* For bottom sheets, this controls whether the user can drag the toolbar handle to
* expand or collapse the sheet. For side sheets, this controls whether the sheet
* can be resized. By default, the Partial Custom Tab is resizable.
* Pass {@code false} to lock the size. Only takes effect when
* {@link #withInitialHeight(int)} is also set.
*
* @param resizable whether the Partial Custom Tab should be resizable.
* @return this same builder instance.
*/
@NonNull
public Builder withResizable(boolean resizable) {
this.activityHeightResizeBehavior = resizable
? CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE
: CustomTabsIntent.ACTIVITY_HEIGHT_FIXED;
return this;
}

/**
* Sets the toolbar's top corner radii in dp. Only takes effect when the Custom Tab is
* displayed as a bottom sheet (i.e., when {@link #withInitialHeight(int)} is also set).
* Pass the size in dp. The underlying
* {@link CustomTabsIntent.Builder#setToolbarCornerRadiusDp(int)} only accepts values in
* the range {@code 0}&ndash;{@code 16} (inclusive); to avoid a runtime crash, values
* outside that range are clamped: negative values are treated as {@code 0} and values
* greater than {@code 16} are capped to {@code 16}.
*
* @param cornerRadius the toolbar's top corner radius in dp. Clamped to {@code [0, 16]}.
* @return this same builder instance.
*/
@NonNull
public Builder withToolbarCornerRadius(@Dimension(unit = Dimension.DP) int cornerRadius) {
Comment thread
subhankarmaiti marked this conversation as resolved.
if (cornerRadius < 0) {
cornerRadius = 0;
}
if (cornerRadius > 16) {
cornerRadius = 16;
}
this.toolbarCornerRadius = cornerRadius;
return this;
}

/**
* Sets the initial width for the Custom Tab to display as a side sheet on larger screens.
* The Custom Tab will behave as a side sheet only if the screen's width is bigger than
* the breakpoint value set by {@link #withSideSheetBreakpoint(int)}. If no breakpoint is
* explicitly set, the browser's default breakpoint (typically 840dp in Chrome) is used,
* so smaller-width devices will continue to render as a bottom sheet or full screen
* rather than as a side sheet.
* Pass the size in dp; it will be converted to pixels internally.
* Falls back to bottom sheet or full screen on unsupported browsers.
*
* @param width the initial side sheet width in dp.
* @return this same builder instance.
*/
@NonNull
public Builder withInitialWidth(@Dimension(unit = Dimension.DP) int width) {
this.initialWidth = width;
return this;
}

/**
* Sets the breakpoint in dp to switch between bottom sheet and side sheet mode.
* If the screen's width is bigger than this value, the Custom Tab will behave as a side sheet;
* otherwise it will behave as a bottom sheet.
* <p>
* When this method is not called (or the value is left at the default {@code 0}), the
* breakpoint is <b>not</b> overridden and the browser's built-in default (typically
* {@code 840dp} in Chrome) is applied. This means devices with a screen width smaller
* than the browser default will still render as a bottom sheet, not a side sheet.
*
* @param breakpoint the breakpoint in dp.
* @return this same builder instance.
*/
@NonNull
public Builder withSideSheetBreakpoint(@Dimension(unit = Dimension.DP) int breakpoint) {
this.sideSheetBreakpoint = breakpoint;
return this;
}

/**
* Enables or disables interaction with the background app when a Partial Custom Tab is displayed.
* By default, background interaction is disabled.
*
* @param enabled whether to enable interaction with the app behind the partial tab.
* @return this same builder instance.
*/
@NonNull
public Builder withBackgroundInteractionEnabled(boolean enabled) {
this.backgroundInteractionEnabled = enabled;
return this;
}

/**
* Create a new CustomTabsOptions instance with the customization settings.
*
* @return an instance of CustomTabsOptions with the customization settings.
*/
@NonNull
public CustomTabsOptions build() {
return new CustomTabsOptions(showTitle, toolbarColor, browserPicker, disabledCustomTabsPackages);
return new CustomTabsOptions(showTitle, toolbarColor, browserPicker, disabledCustomTabsPackages,
initialHeight, activityHeightResizeBehavior, toolbarCornerRadius,
initialWidth, sideSheetBreakpoint, backgroundInteractionEnabled);
}
}
private int dpToPx(@NonNull Context context, int dp) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return Math.round(dp * metrics.density);
}

}
Loading
Loading