From b6a497b74574984f4a015f09496924b929f90095 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Tue, 31 Oct 2023 11:22:47 +1300 Subject: [PATCH] feat: add android custom tabs dismiss listener for pkce flow --- Source/Immutable/Immutable_UPL_Android.xml | 17 +++++++ .../Immutable/Android/ImmutableAndroidJNI.cpp | 6 +++ .../Immutable/Android/ImmutableAndroidJNI.h | 4 ++ .../Android/Java/ImmutableAndroid.java | 17 ++++++- .../Private/Immutable/ImmutablePassport.cpp | 45 +++++++++++++++++++ .../Public/Immutable/ImmutablePassport.h | 10 +++++ 6 files changed, 97 insertions(+), 2 deletions(-) diff --git a/Source/Immutable/Immutable_UPL_Android.xml b/Source/Immutable/Immutable_UPL_Android.xml index 4552aec..10aafed 100644 --- a/Source/Immutable/Immutable_UPL_Android.xml +++ b/Source/Immutable/Immutable_UPL_Android.xml @@ -23,6 +23,16 @@ + + + import com.immutable.unreal.ImmutableAndroid; + + + + + ImmutableAndroid.Callback, + + Uri uri = getIntent().getData(); @@ -44,6 +54,13 @@ public native void handleDeepLink(String Deeplink); + + public native void handleOnCustomTabsDismissed(); + + @Override + public void onCustomTabsDismissed() { + handleOnCustomTabsDismissed(); + } diff --git a/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.cpp b/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.cpp index bcf4243..7b7fa25 100644 --- a/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.cpp +++ b/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.cpp @@ -14,4 +14,10 @@ Java_com_epicgames_unreal_GameActivity_handleDeepLink(JNIEnv *env, jobject obj, UImmutablePassport::HandleDeepLink(deeplink); env->ReleaseStringUTFChars(jDeeplink, deeplinkCStr); } + +JNI_METHOD void +Java_com_epicgames_unreal_GameActivity_handleOnCustomTabsDismissed( + JNIEnv *env, jobject obj) { + UImmutablePassport::HandleCustomTabsDismissed(); +} #endif \ No newline at end of file diff --git a/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.h b/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.h index 40212cc..386cd50 100644 --- a/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.h +++ b/Source/Immutable/Private/Immutable/Android/ImmutableAndroidJNI.h @@ -7,6 +7,10 @@ extern "C" { JNI_METHOD void Java_com_epicgames_unreal_GameActivity_handleDeepLink(JNIEnv *, jobject, jstring); + +JNI_METHOD void +Java_com_epicgames_unreal_GameActivity_handleOnCustomTabsDismissed(JNIEnv *, + jobject); } #endif diff --git a/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java b/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java index 44e3e77..7e124ec 100644 --- a/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java +++ b/Source/Immutable/Private/Immutable/Android/Java/ImmutableAndroid.java @@ -8,11 +8,13 @@ import android.graphics.Insets; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.util.DisplayMetrics; import android.view.WindowInsets; import android.view.WindowMetrics; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabsCallback; import androidx.browser.customtabs.CustomTabsClient; import androidx.browser.customtabs.CustomTabsIntent; @@ -68,7 +70,14 @@ public void onServiceDisconnected(ComponentName name) { @Override public void onCustomTabsServiceConnected(@NonNull ComponentName name, @NonNull CustomTabsClient client) { - CustomTabsSession session = client.newSession(new CustomTabsCallback()); + CustomTabsSession session = client.newSession(new CustomTabsCallback() { + @Override + public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) { + if (context instanceof Callback && navigationEvent == CustomTabsCallback.TAB_HIDDEN) { + ((Callback) context).onCustomTabsDismissed(); + } + } + }); // Need to set the session to get custom tabs to show as a bottom sheet CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder(session) .setInitialActivityHeightPx(getCustomTabsHeight(context)) @@ -84,4 +93,8 @@ public void onCustomTabsServiceConnected(@NonNull ComponentName name, @NonNull C context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } } -} + + public interface Callback { + void onCustomTabsDismissed(); + } +} \ No newline at end of file diff --git a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp index 384c674..91ac586 100644 --- a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp +++ b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp @@ -229,6 +229,10 @@ void UImmutablePassport::ConfirmCode( #if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC void UImmutablePassport::ConnectPKCE( const FImtblPassportResponseDelegate &ResponseDelegate) { +#if PLATFORM_ANDROID + completingPKCE = false; +#endif + PKCEResponseDelegate = ResponseDelegate; CallJS(ImmutablePassportAction::GetPKCEAuthUrl, TEXT(""), PKCEResponseDelegate, @@ -487,6 +491,9 @@ void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response) { Msg = Response.JsonObject->GetStringField(TEXT("result")) .Replace(TEXT(" "), TEXT("+")); #if PLATFORM_ANDROID + OnPKCEDismissed = FImtblPassportOnPKCEDismissedDelegate::CreateUObject( + this, &UImmutablePassport::HandleOnPKCEDismissed); + JNIEnv *Env = FAndroidApplication::GetJavaEnv(); if (Env) { jstring jurl = Env->NewStringUTF(TCHAR_TO_UTF8(*Msg)); @@ -539,6 +546,9 @@ void UImmutablePassport::OnConnectPKCEResponse(FImtblJSResponse Response) { } else { IMTBL_ERR("Unable to return a response for Connect PKCE"); } +#if PLATFORM_ANDROID + completingPKCE = false; +#endif } #endif @@ -737,6 +747,10 @@ void UImmutablePassport::OnDeepLinkActivated(FString DeepLink) { } void UImmutablePassport::CompletePKCEFlow(FString Url) { +#if PLATFORM_ANDROID + completingPKCE = true; +#endif + // Get code and state from deeplink URL TOptional Code, State; FString Endpoint, Params; @@ -761,6 +775,9 @@ void UImmutablePassport::CompletePKCEFlow(FString Url) { PKCEResponseDelegate.ExecuteIfBound( FImmutablePassportResult{false, ErrorMsg}); PKCEResponseDelegate = nullptr; +#if PLATFORM_ANDROID + completingPKCE = false; +#endif } else { FImmutablePassportConnectPKCEData Data = FImmutablePassportConnectPKCEData{Code.GetValue(), State.GetValue()}; @@ -790,6 +807,34 @@ void UImmutablePassport::HandleDeepLink(NSString *sDeepLink) { #endif #if PLATFORM_ANDROID +void UImmutablePassport::HandleOnPKCEDismissed() { + IMTBL_LOG("Handle On PKCE Dismissed"); + FFunctionGraphTask::CreateAndDispatchWhenReady( + [=]() { + OnPKCEDismissed = nullptr; + if (!completingPKCE) { + // User hasn't entered all required details (e.g. email address) into + // Passport yet + IMTBL_LOG("PKCE dismissed before completing the flow"); + if (!PKCEResponseDelegate.ExecuteIfBound( + FImmutablePassportResult{false, "Cancelled"})) { + IMTBL_WARN("PKCEResponseDelegate delegate was not called"); + } + PKCEResponseDelegate = nullptr; + } else { + IMTBL_LOG("PKCE dismissed by user or SDK"); + } + }, + TStatId(), nullptr, ENamedThreads::GameThread); +} + +void UImmutablePassport::HandleCustomTabsDismissed() { + IMTBL_LOG("On PKCE Dismissed"); + if (!OnPKCEDismissed.ExecuteIfBound()) { + IMTBL_WARN("OnPKCEDismissed delegate was not called"); + } +} + void UImmutablePassport::CallJniStaticVoidMethod(JNIEnv *Env, const jclass Class, jmethodID Method, ...) { diff --git a/Source/Immutable/Public/Immutable/ImmutablePassport.h b/Source/Immutable/Public/Immutable/ImmutablePassport.h index 5fa079a..8ebd96d 100644 --- a/Source/Immutable/Public/Immutable/ImmutablePassport.h +++ b/Source/Immutable/Public/Immutable/ImmutablePassport.h @@ -401,6 +401,10 @@ struct FImmutablePassportConnectPKCEData { DECLARE_DELEGATE_OneParam(FImtblPassportHandleDeepLinkDelegate, FString); FImtblPassportHandleDeepLinkDelegate OnHandleDeepLink; #endif +#if PLATFORM_ANDROID +DECLARE_DELEGATE(FImtblPassportOnPKCEDismissedDelegate); +FImtblPassportOnPKCEDismissedDelegate OnPKCEDismissed; +#endif /** * @@ -418,6 +422,7 @@ class IMMUTABLE_API UImmutablePassport : public UObject { #if PLATFORM_ANDROID static void HandleDeepLink(FString DeepLink); + static void HandleCustomTabsDismissed(); #elif PLATFORM_IOS | PLATFORM_MAC static void HandleDeepLink(NSString *sDeepLink); #endif @@ -503,6 +508,10 @@ class IMMUTABLE_API UImmutablePassport : public UObject { bool bIsInitialized = false; bool bIsLoggedIn = false; +#if PLATFORM_ANDROID + bool completingPKCE = false; // Used for the PKCE callback +#endif + TWeakObjectPtr JSConnector; FImmutablePassportInitData InitData; FDelegateHandle BridgeReadyHandle; @@ -557,6 +566,7 @@ class IMMUTABLE_API UImmutablePassport : public UObject { #endif #if PLATFORM_ANDROID + void HandleOnPKCEDismissed(); void CallJniStaticVoidMethod(JNIEnv *Env, const jclass Class, jmethodID Method, ...); #endif