Skip to content

chore: PKCE for Windows support (alpha release) #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 5, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void UImtblConnectionAsyncActions::DoConnect(TWeakObjectPtr<UImtblJSConnector> J
{
if (bIsPKCE)
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
Passport->ConnectPKCE(bIsConnectImx, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UImtblConnectionAsyncActions::OnConnect));
#endif
}
Expand Down
27 changes: 22 additions & 5 deletions Source/Immutable/Private/Immutable/ImmutableDataTypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#include "Immutable/ImmutableDataTypes.h"

#if PLATFORM_WINDOWS
#include "Immutable/Windows/ImmutablePKCEWindows.h"
#endif

FString FImmutablePassportInitData::ToJsonString() const
{
Expand Down Expand Up @@ -29,20 +32,20 @@ FString FImmutablePassportInitData::ToJsonString() const
TOptional<FImmutablePassportInitDeviceFlowData> FImmutablePassportInitDeviceFlowData::FromJsonString(const FString& JsonObjectString)
{
FImmutablePassportInitDeviceFlowData PassportConnect;

if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonObjectString, &PassportConnect, 0, 0))
{
IMTBL_WARN("Could not parse response from JavaScript into the expected " "Passport connect format")
return TOptional<FImmutablePassportInitDeviceFlowData>();
}

return PassportConnect;
}

FString FImmutablePassportZkEvmRequestAccountsData::ToJsonString() const
{
FString OutString;

FJsonObjectConverter::UStructToJsonObjectString(*this, OutString, 0, 0, 0, nullptr, false);

return OutString;
Expand All @@ -56,7 +59,7 @@ TOptional<FImmutablePassportZkEvmRequestAccountsData> FImmutablePassportZkEvmReq
IMTBL_WARN("Could not parse response from JavaScript into the expected " "Passport ZkEvm request accounts format")
return TOptional<FImmutablePassportZkEvmRequestAccountsData>();
}

return RequestAccounts;
}

Expand All @@ -71,7 +74,7 @@ TOptional<FImmutablePassportZkEvmRequestAccountsData> FImmutablePassportZkEvmReq
IMTBL_ERR("Could not parse response from JavaScript into the expected " "Passport ZkEvm request accounts format")
return TOptional<FImmutablePassportZkEvmRequestAccountsData>();
}

return RequestAccounts;
}

Expand All @@ -92,3 +95,17 @@ FString FImmutablePassportZkEvmGetBalanceData::ToJsonString() const

return OutString;
}

void UImmutablePKCEData::BeginDestroy()
{
Reset();

UObject::BeginDestroy();
}

void UImmutablePKCEData::Reset()
{
#if PLATFORM_WINDOWS
UImmutablePKCEWindows::Reset(this);
#endif
}
120 changes: 84 additions & 36 deletions Source/Immutable/Private/Immutable/ImmutablePassport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
#include "GenericPlatform/GenericPlatformProcess.h"
#include "Mac/ImmutableMac.h"
#endif
#if PLATFORM_WINDOWS
#include "Immutable/Windows/ImmutablePKCEWindows.h"
#endif

#define PASSPORT_SAVE_GAME_SLOT_NAME TEXT("Immutable")

Expand Down Expand Up @@ -101,10 +104,22 @@ void UImmutablePassport::Connect(bool IsConnectImx, bool TryToRelogin, const FIm
}
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
void UImmutablePassport::ConnectPKCE(bool IsConnectImx, const FImtblPassportResponseDelegate& ResponseDelegate)
{
SetStateFlags(IPS_CONNECTING | IPS_PKCE);

#if PLATFORM_WINDOWS
// Verify PKCEData is null before initializing to ensure we're not overriding an active PKCE operation.
// A non-null value indicates another PKCE operation is already in progress.
ensureAlways(!PKCEData);
PKCEData = UImmutablePKCEWindows::Initialise(InitData);
if (PKCEData)
{
PKCEData->DynamicMulticastDelegate_DeepLinkCallback.AddDynamic(this, &ThisClass::OnDeepLinkActivated);
}
#endif

if (IsConnectImx)
{
SetStateFlags(IPS_IMX);
Expand All @@ -117,7 +132,17 @@ void UImmutablePassport::ConnectPKCE(bool IsConnectImx, const FImtblPassportResp

void UImmutablePassport::Logout(bool DoHardLogout, const FImtblPassportResponseDelegate& ResponseDelegate)
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_WINDOWS
// Verify PKCEData is null before initializing to ensure we're not overriding an active PKCE operation.
// A non-null value indicates another PKCE operation is already in progress.
ensureAlways(!PKCEData);
PKCEData = UImmutablePKCEWindows::Initialise(InitData);
if (PKCEData)
{
PKCEData->DynamicMulticastDelegate_DeepLinkCallback.AddDynamic(this, &ThisClass::OnDeepLinkActivated);
}
#endif
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
if (IsStateFlagsSet(IPS_PKCE))
{
PKCELogoutResponseDelegate = ResponseDelegate;
Expand Down Expand Up @@ -392,7 +417,7 @@ TOptional<UImmutablePassport::FImtblPassportResponseDelegate> UImmutablePassport

void UImmutablePassport::OnInitializeResponse(FImtblJSResponse Response)
{
if (auto ResponseDelegate = GetResponseDelegate(Response))
if (TOptional<FImtblPassportResponseDelegate> ResponseDelegate = GetResponseDelegate(Response))
{
FString Error;

Expand Down Expand Up @@ -470,43 +495,55 @@ void UImmutablePassport::OnLogoutResponse(FImtblJSResponse Response)

return;
}

FString Url;
FString ErrorMessage;

auto Logout = [this](const FImtblJSResponse& Response)
{
TOptional<FImtblPassportResponseDelegate> ResponseDelegate = GetResponseDelegate(Response);

FString Url;
Response.JsonObject->TryGetStringField(TEXT("result"), Url);

FString ErrorMessage;
FPlatformProcess::LaunchURL(*Url, nullptr, &ErrorMessage);

if (ErrorMessage.Len())
{
ErrorMessage = "Failed to launch browser: " + ErrorMessage;
IMTBL_ERR("%s", *ErrorMessage);
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{false, ErrorMessage, Response});
}
};

ResetStateFlags(IPS_HARDLOGOUT);

FString Url;
Response.JsonObject->TryGetStringField(TEXT("result"), Url);

if (!Url.IsEmpty())
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
if (IsStateFlagsSet(IPS_PKCE))
{
OnHandleDeepLink = FImtblPassportHandleDeepLinkDelegate::CreateUObject(this, &UImmutablePassport::OnDeepLinkActivated);
OnHandleDeepLink.AddUObject(this, &UImmutablePassport::OnDeepLinkActivated);
#if PLATFORM_ANDROID
LaunchAndroidUrl(Url);
#elif PLATFORM_IOS
[[ImmutableIOS instance] launchUrl:TCHAR_TO_ANSI(*Url)];
#elif PLATFORM_MAC
[[ImmutableMac instance] launchUrl:TCHAR_TO_ANSI(*Url) forRedirectUri:TCHAR_TO_ANSI(*InitData.logoutRedirectUri)];
#endif
#if PLATFORM_WINDOWS
Logout(Response);
#endif
}
else
{
#endif
FPlatformProcess::LaunchURL(*Url, nullptr, &ErrorMessage);
if (ErrorMessage.Len())
{
Message = "Failed to connect to Browser: " + ErrorMessage;

IMTBL_ERR("%s", *Message);
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{ false, Message, Response });

return;
}
Logout(Response);
Analytics->Track(UImmutableAnalytics::EEventName::COMPLETE_LOGOUT);
IMTBL_LOG("Logged out")
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{ Response.success });
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
}
#endif
}
Expand All @@ -517,7 +554,7 @@ void UImmutablePassport::OnLogoutResponse(FImtblJSResponse Response)
ResetStateFlags(IPS_CONNECTED);
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
{
if (PKCEResponseDelegate.IsBound())
Expand All @@ -532,7 +569,7 @@ void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
else
{
// Handle deeplink calls
OnHandleDeepLink = FImtblPassportHandleDeepLinkDelegate::CreateUObject(this, &UImmutablePassport::OnDeepLinkActivated);
OnHandleDeepLink.AddUObject(this, &UImmutablePassport::OnDeepLinkActivated);

Msg = Response.JsonObject->GetStringField(TEXT("result")).Replace(TEXT(" "), TEXT("+"));
#if PLATFORM_ANDROID
Expand All @@ -542,6 +579,17 @@ void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
[[ImmutableIOS instance] launchUrl:TCHAR_TO_ANSI(*Msg)];
#elif PLATFORM_MAC
[[ImmutableMac instance] launchUrl:TCHAR_TO_ANSI(*Msg) forRedirectUri:TCHAR_TO_ANSI(*InitData.redirectUri)];
#elif PLATFORM_WINDOWS
FString ErrorMessage;
FPlatformProcess::LaunchURL(*Msg, nullptr, &ErrorMessage);
if (!ErrorMessage.IsEmpty())
{
ErrorMessage = "Failed to launch browser: " + ErrorMessage;
IMTBL_ERR("%s", *ErrorMessage);
PKCEResponseDelegate.ExecuteIfBound(FImmutablePassportResult{false, ErrorMessage});
PKCEResponseDelegate.Unbind();
ResetStateFlags(IPS_PKCE | IPS_CONNECTING);
}
#endif
}
}
Expand Down Expand Up @@ -583,7 +631,6 @@ void UImmutablePassport::OnConnectPKCEResponse(FImtblJSResponse Response)
}
ResetStateFlags(IPS_COMPLETING_PKCE);
}
#endif

void UImmutablePassport::OnConfirmCodeResponse(FImtblJSResponse Response)
{
Expand Down Expand Up @@ -666,11 +713,9 @@ void UImmutablePassport::LoadPassportSettings()
}
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
void UImmutablePassport::OnDeepLinkActivated(FString DeepLink)
void UImmutablePassport::OnDeepLinkActivated(const FString& DeepLink)
{
IMTBL_LOG_FUNC("URL : %s", *DeepLink);
OnHandleDeepLink = nullptr;
OnHandleDeepLink.Clear();
if (DeepLink.StartsWith(InitData.logoutRedirectUri))
{
// execute on game thread to prevent call to Passport instance from another thread
Expand All @@ -690,6 +735,8 @@ void UImmutablePassport::OnDeepLinkActivated(FString DeepLink)
{
CompleteLoginPKCEFlow(DeepLink);
}

PKCEData = nullptr;
}

void UImmutablePassport::CompleteLoginPKCEFlow(FString Url)
Expand Down Expand Up @@ -739,30 +786,31 @@ void UImmutablePassport::CompleteLoginPKCEFlow(FString Url)
FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnConnectPKCEResponse));
}
}

#endif

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID
#if PLATFORM_ANDROID | PLATFORM_WINDOWS
// Called from Android JNI
void UImmutablePassport::HandleDeepLink(FString DeepLink) const
{
#elif PLATFORM_IOS | PLATFORM_MAC

#endif
#if PLATFORM_IOS | PLATFORM_MAC
// Called from iOS Objective C
void UImmutablePassport::HandleDeepLink(NSString* sDeepLink) const
#endif
{
#if PLATFORM_IOS | PLATFORM_MAC
FString DeepLink = FString(UTF8_TO_TCHAR([sDeepLink UTF8String]));
IMTBL_LOG("Handle Deep Link: %s", *DeepLink);
#endif

if (!OnHandleDeepLink.ExecuteIfBound(DeepLink))
#if PLATFORM_WINDOWS
if (PKCEData)
{
IMTBL_WARN("OnHandleDeepLink delegate was not called");
UImmutablePKCEWindows::HandleDeepLink(PKCEData, DeepLink);
}
}
#endif

OnHandleDeepLink.Broadcast(DeepLink);
}

#if PLATFORM_ANDROID
void UImmutablePassport::HandleOnLoginPKCEDismissed()
{
Expand Down
Loading
Loading