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
4 changes: 4 additions & 0 deletions example/capacitor/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ function AuthgearDemo() {
modalPresentationStyle: "fullScreen",
isInspectable: true,
},
android: {
actionBarBackgroundColor: 0x00ffffff,
actionBarButtonTintColor: 0xff000000,
},
})
: undefined,
isSSOEnabled,
Expand Down
2 changes: 2 additions & 0 deletions example/reactnative/src/screens/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ const HomeScreen: React.FC = () => {
},
android: {
wechatRedirectURI,
actionBarBackgroundColor: 0x00ffffff,
actionBarButtonTintColor: 0xff000000,
},
sendWechatAuthRequest,
})
Expand Down
1 change: 1 addition & 0 deletions packages/authgear-capacitor/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation 'androidx.core:core:1.12.0'
implementation 'androidx.browser:browser:1.2.0'
implementation "androidx.biometric:biometric:1.2.0-alpha05"
// NOTE(backup): Please search NOTE(backup) before you update security-crypto or tink-android.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,41 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;

import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.Insets;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.util.TypedValueCompat;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class WebKitWebViewActivity extends AppCompatActivity {

Expand All @@ -38,7 +50,11 @@ public class WebKitWebViewActivity extends AppCompatActivity {
private static final int MENU_ID_CANCEL = 1;
private static final int TAG_FILE_CHOOSER = 1;

private FrameLayout mRootFrameLayout;
private Toolbar mToolbar;
private FrameLayout mToolbarFrameLayout;
private WebView mWebView;
private Insets mLastSeenInsets;
private Uri result;
private StartActivityHandles<ValueCallback<Uri[]>> handles = new StartActivityHandles<>();

Expand Down Expand Up @@ -85,6 +101,19 @@ private MyWebViewClient(WebKitWebViewActivity activity) {
this.activity = activity;
}

@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// onPageStarted is not always called, but when it is called, it is called before
// onPageFinished.
// Therefore, we put the edge-to-edge handling here hoping that
// the safe area insets can be set as soon as possible.

view.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE, null);
this.activity.handleNonEdgeToEdge();
this.activity.handleEdgeToEdge();
}

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Expand All @@ -96,6 +125,8 @@ public void onPageFinished(WebView view, String url) {
// The caveat is that the script is run in the main frame only.
// But we do not actually use iframes so it does not matter.
view.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE, null);
this.activity.handleNonEdgeToEdge();
this.activity.handleEdgeToEdge();
}

@TargetApi(Build.VERSION_CODES.N)
Expand Down Expand Up @@ -199,18 +230,85 @@ public static Intent createIntent(Context ctx, Options options) {
return intent;
}

private float getActionBarSizeInDp() {
float actionBarSizeInDp = 44f;
TypedValue tv = new TypedValue();
if (this.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
int actionBarSizeInPx = TypedValue.complexToDimensionPixelSize(tv.data, this.getResources().getDisplayMetrics());
actionBarSizeInDp = TypedValueCompat.pxToDp((float) actionBarSizeInPx, this.getResources().getDisplayMetrics());
}
return actionBarSizeInDp;
}

private void applyInsetsToWebView(Insets safeAreaInsets) {
float actionBarSizeInDp = this.getActionBarSizeInDp();
DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
float actionBarSizeInPx = TypedValueCompat.dpToPx(actionBarSizeInDp, displayMetrics);
float top = TypedValueCompat.pxToDp((float) safeAreaInsets.top + actionBarSizeInPx, displayMetrics);
float right = TypedValueCompat.pxToDp((float) safeAreaInsets.right, displayMetrics);
float bottom = TypedValueCompat.pxToDp((float) safeAreaInsets.bottom, displayMetrics);
float left = TypedValueCompat.pxToDp((float) safeAreaInsets.left, displayMetrics);

String safeAreaJs =
"document.documentElement.style.setProperty('--safe-area-inset-top', '" + top + "px');\n" +
"document.documentElement.style.setProperty('--safe-area-inset-right', '" + right + "px');\n" +
"document.documentElement.style.setProperty('--safe-area-inset-bottom', '" + bottom + "px');\n" +
"document.documentElement.style.setProperty('--safe-area-inset-left', '" + left + "px');";

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
this.mWebView.evaluateJavascript(safeAreaJs, null);
}
}

private void handleNonEdgeToEdge() {
// In non edge-to-edge, the insets listener is not called.
// So we have to apply the insets here.
Insets insets = this.mLastSeenInsets == null ? Insets.NONE : this.mLastSeenInsets;
this.applyInsetsToWebView(insets);
}

private void handleEdgeToEdge() {
// In edge-to-edge, we ask the system to invoke the insets listener.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
this.mRootFrameLayout.requestApplyInsets();
}
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

this.mRootFrameLayout = new FrameLayout(this);
this.mRootFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));

this.mToolbarFrameLayout = new FrameLayout(this);
this.mToolbarFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));

float actionBarSizeInDp = this.getActionBarSizeInDp();

this.mToolbar = new Toolbar(this);
this.mToolbar.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
(int) TypedValueCompat.dpToPx(actionBarSizeInDp, this.getResources().getDisplayMetrics())
));
this.setSupportActionBar(this.mToolbar);

Options options = this.getOptions();

// Do not show title.
getSupportActionBar().setDisplayShowTitleEnabled(false);

// Configure navigation bar background color.
if (options.actionBarBackgroundColor != null) {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(options.actionBarBackgroundColor));
ColorDrawable colorDrawable = new ColorDrawable(options.actionBarBackgroundColor);
getSupportActionBar().setBackgroundDrawable(colorDrawable);
this.mToolbarFrameLayout.setBackgroundDrawable(colorDrawable);
}

// Show back button.
Expand All @@ -225,17 +323,62 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

// Configure web view.
this.mWebView = new WebView(this);
this.mWebView.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
this.mWebView.getSettings().setSupportMultipleWindows(true);
this.mWebView.getSettings().setDomStorageEnabled(true);
this.setContentView(this.mWebView);
this.mWebView.setWebViewClient(new MyWebViewClient(this));
this.mWebView.setWebChromeClient(new MyWebChromeClient(this));
WebSettings webSettings = this.mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);

if (savedInstanceState == null) {
this.mWebView.loadUrl(options.url.toString());
}
this.mRootFrameLayout.addView(this.mWebView);
this.mRootFrameLayout.addView(this.mToolbarFrameLayout);
this.mToolbarFrameLayout.addView(this.mToolbar);
this.setContentView(this.mRootFrameLayout);

ViewCompat.setOnApplyWindowInsetsListener(this.mRootFrameLayout, new OnApplyWindowInsetsListener() {

@Override
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
Insets safeAreaInsets = insets.getInsets(
WindowInsetsCompat.Type.systemBars() |
WindowInsetsCompat.Type.displayCutout() |
WindowInsetsCompat.Type.ime()
);

WebKitWebViewActivity.this.mLastSeenInsets = safeAreaInsets;

ViewGroup.MarginLayoutParams toolbarParams = (ViewGroup.MarginLayoutParams) mToolbar.getLayoutParams();
toolbarParams.setMargins(
safeAreaInsets.left,
safeAreaInsets.top,
safeAreaInsets.right,
0
);

WebKitWebViewActivity.this.applyInsetsToWebView(safeAreaInsets);

return WindowInsetsCompat.CONSUMED;
}
});

this.mRootFrameLayout.post(new Runnable() {
@Override
public void run() {
// We want the content view to draw at least once before loading the URL.
//
// In non edge-to-edge, the insets listener is never called so mLastSeenInsets is null.
//
// In edge-to-edge, the insets listener will be called at least once in the first draw,
// so by the time onPageStart / onPageFinished is called, mLastSeenInsets is not null.
if (savedInstanceState == null) {
WebKitWebViewActivity.this.mWebView.loadUrl(options.url.toString());
}
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<resources>
<style name="AuthgearTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
<style name="AuthgearTheme" parent="Theme.AppCompat.Light.NoActionBar">
</style>
</resources>
1 change: 1 addition & 0 deletions packages/authgear-react-native/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules

implementation 'androidx.core:core:1.12.0'
implementation 'androidx.browser:browser:1.2.0'
implementation "androidx.biometric:biometric:1.2.0-alpha03"
// NOTE(backup): Please search NOTE(backup) before you update security-crypto or tink-android.
Expand Down
Loading