diff --git a/Assets/Plugins/iOS/Common.h b/Assets/Plugins/iOS/Common.h new file mode 100644 index 00000000..d4a73fcb --- /dev/null +++ b/Assets/Plugins/iOS/Common.h @@ -0,0 +1,40 @@ +#ifndef Common_h +#define Common_h + +typedef int bool_t; + +inline bool_t toBool(bool v) +{ + return v ? 1 : 0; +} + +inline bool toBool(bool_t v) +{ + return v != 0; +} + +inline NSString* toString(const char* string) +{ + if (string != NULL) + { + return [NSString stringWithUTF8String:string]; + } + else + { + return [NSString stringWithUTF8String:""]; + } +} + +inline char* toString(NSString* string) +{ + const char* cstr = [string UTF8String]; + + if (cstr == NULL) + return NULL; + + char* copy = (char*)malloc(strlen(cstr) + 1); + strcpy(copy, cstr); + return copy; +} + +#endif /* Common_h */ diff --git a/Assets/Plugins/iOS/Common.h.meta b/Assets/Plugins/iOS/Common.h.meta new file mode 100644 index 00000000..c71d5883 --- /dev/null +++ b/Assets/Plugins/iOS/Common.h.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 933a67f10ef30d144a6f715bc79a4b5b +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/iOSBrowser.m b/Assets/Plugins/iOS/iOSBrowser.m deleted file mode 100644 index 05dc210b..00000000 --- a/Assets/Plugins/iOS/iOSBrowser.m +++ /dev/null @@ -1,30 +0,0 @@ -#import -#import -#import - -@interface iOSBrowser : NSObject -@end - -@implementation iOSBrowser - -static UIViewController* GetCurrentViewController() { - UIWindow *window = [[UIApplication sharedApplication] keyWindow]; - UIViewController *rootViewController = window.rootViewController; - - UIViewController *currentController = rootViewController; - while (currentController.presentedViewController) { - currentController = currentController.presentedViewController; - } - return currentController; -} - -void _OpenURL(const char* url) { - NSString *urlString = [NSString stringWithUTF8String:url]; - NSURL *nsURL = [NSURL URLWithString:urlString]; - - SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:nsURL]; - UIViewController *currentViewController = GetCurrentViewController(); - [currentViewController presentViewController:safariViewController animated:YES completion:nil]; -} - -@end diff --git a/Assets/Plugins/iOS/iOSBrowser.mm b/Assets/Plugins/iOS/iOSBrowser.mm new file mode 100644 index 00000000..536f61a2 --- /dev/null +++ b/Assets/Plugins/iOS/iOSBrowser.mm @@ -0,0 +1,97 @@ +#import + +#include "Common.h" + +extern UIViewController* UnityGetGLViewController(); + +typedef void (*ASWebAuthenticationSessionCompletionCallback)(void* sessionPtr, const char* callbackUrl, int errorCode, const char* errorMessage); + +@interface Thirdweb_ASWebAuthenticationSession : NSObject + +@property (readonly, nonatomic)ASWebAuthenticationSession* session; + +@end + +@implementation Thirdweb_ASWebAuthenticationSession + +- (instancetype)initWithURL:(NSURL *)URL callbackURLScheme:(nullable NSString *)callbackURLScheme completionCallback:(ASWebAuthenticationSessionCompletionCallback)completionCallback +{ + _session = [[ASWebAuthenticationSession alloc] initWithURL:URL + callbackURLScheme: callbackURLScheme + completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error) + { + if (error != nil) + { + NSLog(@"[ASWebAuthenticationSession:CompletionHandler] %@", error.description); + } + else + { + //NSLog(@"[ASWebAuthenticationSession:CompletionHandler] Callback URL: %@", callbackURL); + } + + completionCallback((__bridge void*)self, toString(callbackURL.absoluteString), (int)error.code, toString(error.localizedDescription)); + }]; + + [_session setPresentationContextProvider:self]; + return self; +} + +- (nonnull ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(nonnull ASWebAuthenticationSession *)session +{ + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 || __TV_OS_VERSION_MAX_ALLOWED >= 130000 + return [[[UIApplication sharedApplication] delegate] window]; + #elif __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 + return [[NSApplication sharedApplication] mainWindow]; + #else + return nil; + #endif +} + +@end + +extern "C" +{ + Thirdweb_ASWebAuthenticationSession* Thirdweb_ASWebAuthenticationSession_InitWithURL( + const char* urlStr, const char* urlSchemeStr, ASWebAuthenticationSessionCompletionCallback completionCallback) + { + //NSLog(@"[ASWebAuthenticationSession:InitWithURL] initWithURL: %s callbackURLScheme:%s", urlStr, urlSchemeStr); + + NSURL* url = [NSURL URLWithString: toString(urlStr)]; + NSString* urlScheme = toString(urlSchemeStr); + + Thirdweb_ASWebAuthenticationSession* session = [[Thirdweb_ASWebAuthenticationSession alloc] initWithURL:url + callbackURLScheme: urlScheme + completionCallback:completionCallback]; + return session; + } + + int Thirdweb_ASWebAuthenticationSession_Start(void* sessionPtr) + { + Thirdweb_ASWebAuthenticationSession* session = (__bridge Thirdweb_ASWebAuthenticationSession*) sessionPtr; + BOOL started = [[session session] start]; + + //NSLog(@"[ASWebAuthenticationSession:Start]: %s", (started ? "YES" : "NO")); + + return toBool(started); + } + + void Thirdweb_ASWebAuthenticationSession_Cancel(void* sessionPtr) + { + //NSLog(@"[ASWebAuthenticationSession:Cancel]"); + + Thirdweb_ASWebAuthenticationSession* session = (__bridge Thirdweb_ASWebAuthenticationSession*) sessionPtr; + [[session session] cancel]; + } + + int Thirdweb_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(void* sessionPtr) + { + Thirdweb_ASWebAuthenticationSession* session = (__bridge Thirdweb_ASWebAuthenticationSession*) sessionPtr; + return toBool([[session session] prefersEphemeralWebBrowserSession]); + } + + void Thirdweb_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(void* sessionPtr, int enable) + { + Thirdweb_ASWebAuthenticationSession* session = (__bridge Thirdweb_ASWebAuthenticationSession*) sessionPtr; + [[session session] setPrefersEphemeralWebBrowserSession:toBool(enable)]; + } +} diff --git a/Assets/Plugins/iOS/iOSBrowser.m.meta b/Assets/Plugins/iOS/iOSBrowser.mm.meta similarity index 94% rename from Assets/Plugins/iOS/iOSBrowser.m.meta rename to Assets/Plugins/iOS/iOSBrowser.mm.meta index c1087741..da64cf9b 100644 --- a/Assets/Plugins/iOS/iOSBrowser.m.meta +++ b/Assets/Plugins/iOS/iOSBrowser.mm.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a8b902c78bc2bc9498f63152b1d05ba8 +guid: 2fcb14eac0784fc4290686f8b11245f6 PluginImporter: externalObjects: {} serializedVersion: 2 @@ -76,7 +76,7 @@ PluginImporter: AddToEmbeddedBinaries: false CPU: AnyCPU CompileFlags: - FrameworkDependencies: SafariServices; + FrameworkDependencies: AuthenticationServices; userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Thirdweb/Core/Scripts/Browser/AndroidBrowser.cs b/Assets/Thirdweb/Core/Scripts/Browser/AndroidBrowser.cs index 72fc727a..5253d931 100644 --- a/Assets/Thirdweb/Core/Scripts/Browser/AndroidBrowser.cs +++ b/Assets/Thirdweb/Core/Scripts/Browser/AndroidBrowser.cs @@ -1,4 +1,4 @@ -#if UNITY_ANDROID +#if UNITY_ANDROID && !UNITY_EDITOR using System; using System.Threading; diff --git a/Assets/Thirdweb/Core/Scripts/Browser/CrossPlatformBrowser.cs b/Assets/Thirdweb/Core/Scripts/Browser/CrossPlatformBrowser.cs index 3f07a3a6..2d42bff3 100644 --- a/Assets/Thirdweb/Core/Scripts/Browser/CrossPlatformBrowser.cs +++ b/Assets/Thirdweb/Core/Scripts/Browser/CrossPlatformBrowser.cs @@ -9,9 +9,9 @@ public class CrossPlatformBrowser : IThirdwebBrowser public async Task Login(string loginUrl, string redirectUrl, CancellationToken cancellationToken = default) { -#if UNITY_ANDROID +#if UNITY_ANDROID && !UNITY_EDITOR _browser = new AndroidBrowser(); -#elif UNITY_IOS +#elif UNITY_IOS && !UNITY_EDITOR _browser = new IOSBrowser(); #else _browser = new StandaloneBrowser(); diff --git a/Assets/Thirdweb/Core/Scripts/Browser/IOSBrowser.cs b/Assets/Thirdweb/Core/Scripts/Browser/IOSBrowser.cs index 4bc71821..ed26cc73 100644 --- a/Assets/Thirdweb/Core/Scripts/Browser/IOSBrowser.cs +++ b/Assets/Thirdweb/Core/Scripts/Browser/IOSBrowser.cs @@ -1,67 +1,159 @@ -#if UNITY_IOS +#if UNITY_IOS && !UNITY_EDITOR using System; -using System.Runtime.InteropServices; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using UnityEngine; +using AOT; + +using System.Runtime.InteropServices; namespace Thirdweb.Browser { + public class ASWebAuthenticationSession : IDisposable + { + private static readonly Dictionary CompletionCallbacks = new(); + + private IntPtr _sessionPtr; + + public bool prefersEphemeralWebBrowserSession + { + get => Thirdweb_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(_sessionPtr) == 1; + set => Thirdweb_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(_sessionPtr, value ? 1 : 0); + } + + public ASWebAuthenticationSession(string url, string callbackUrlScheme, ASWebAuthenticationSessionCompletionHandler completionHandler) + { + _sessionPtr = Thirdweb_ASWebAuthenticationSession_InitWithURL(url, callbackUrlScheme, OnAuthenticationSessionCompleted); + + CompletionCallbacks.Add(_sessionPtr, completionHandler); + } + + public bool Start() + { + return Thirdweb_ASWebAuthenticationSession_Start(_sessionPtr) == 1; + } + + public void Cancel() + { + Thirdweb_ASWebAuthenticationSession_Cancel(_sessionPtr); + } + + public void Dispose() + { + CompletionCallbacks.Remove(_sessionPtr); + _sessionPtr = IntPtr.Zero; + } + + private const string DllName = "__Internal"; + + [DllImport(DllName)] + private static extern IntPtr Thirdweb_ASWebAuthenticationSession_InitWithURL(string url, string callbackUrlScheme, AuthenticationSessionCompletedCallback completionHandler); + + [DllImport(DllName)] + private static extern int Thirdweb_ASWebAuthenticationSession_Start(IntPtr session); + + [DllImport(DllName)] + private static extern void Thirdweb_ASWebAuthenticationSession_Cancel(IntPtr session); + + [DllImport(DllName)] + private static extern int Thirdweb_ASWebAuthenticationSession_GetPrefersEphemeralWebBrowserSession(IntPtr session); + + [DllImport(DllName)] + private static extern void Thirdweb_ASWebAuthenticationSession_SetPrefersEphemeralWebBrowserSession(IntPtr session, int enable); + + public delegate void ASWebAuthenticationSessionCompletionHandler(string callbackUrl, ASWebAuthenticationSessionError error); + + private delegate void AuthenticationSessionCompletedCallback(IntPtr session, string callbackUrl, int errorCode, string errorMessage); + + [MonoPInvokeCallback(typeof(AuthenticationSessionCompletedCallback))] + private static void OnAuthenticationSessionCompleted(IntPtr session, string callbackUrl, int errorCode, string errorMessage) + { + if (CompletionCallbacks.TryGetValue(session, out var callback)) + { + callback?.Invoke(callbackUrl, new ASWebAuthenticationSessionError((ASWebAuthenticationSessionErrorCode)errorCode, errorMessage)); + } + } + } + public class IOSBrowser : IThirdwebBrowser { private TaskCompletionSource _taskCompletionSource; - private string _customScheme; + public bool prefersEphemeralWebBrowserSession { get; set; } = false; - public async Task Login(string loginUrl, string customScheme, CancellationToken cancellationToken = default) + public async Task Login(string loginUrl, string redirectUrl, CancellationToken cancellationToken = default) { + if (string.IsNullOrEmpty(loginUrl)) + throw new ArgumentNullException(nameof(loginUrl)); + + if (string.IsNullOrEmpty(redirectUrl)) + throw new ArgumentNullException(nameof(redirectUrl)); + _taskCompletionSource = new TaskCompletionSource(); + redirectUrl = redirectUrl.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries)[0]; + + using var authenticationSession = new ASWebAuthenticationSession(loginUrl, redirectUrl, AuthenticationSessionCompletionHandler); + authenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession; + cancellationToken.Register(() => { _taskCompletionSource?.TrySetCanceled(); }); - _customScheme = customScheme; - - Application.deepLinkActivated += OnDeepLinkActivated; - try { - OpenURL(loginUrl); - var completedTask = await Task.WhenAny(_taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(60))); - if (completedTask == _taskCompletionSource.Task) + if (!authenticationSession.Start()) { - return await _taskCompletionSource.Task; - } - else - { - return new BrowserResult(BrowserStatus.Timeout, null, "The operation timed out."); + _taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.UnknownError, "Browser could not be started.")); } + + return await _taskCompletionSource.Task; } - finally + catch (TaskCanceledException) { - Application.deepLinkActivated -= OnDeepLinkActivated; + authenticationSession?.Cancel(); + throw; } } - [DllImport("__Internal")] - private static extern void _OpenURL(string url); - - public void OpenURL(string url) + private void AuthenticationSessionCompletionHandler(string callbackUrl, ASWebAuthenticationSessionError error) { - _OpenURL(url); + if (error.code == ASWebAuthenticationSessionErrorCode.None) + { + _taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.Success, callbackUrl)); + } + else if (error.code == ASWebAuthenticationSessionErrorCode.CanceledLogin) + { + _taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.UserCanceled, callbackUrl, error.message)); + } + else + { + _taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.UnknownError, callbackUrl, error.message)); + } } + } - private void OnDeepLinkActivated(string url) - { - if (!url.StartsWith(_customScheme)) - return; + public class ASWebAuthenticationSessionError + { + public ASWebAuthenticationSessionErrorCode code { get; } + public string message { get; } - _taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.Success, url)); + public ASWebAuthenticationSessionError(ASWebAuthenticationSessionErrorCode code, string message) + { + this.code = code; + this.message = message; } } + + public enum ASWebAuthenticationSessionErrorCode + { + None = 0, + CanceledLogin = 1, + PresentationContextNotProvided = 2, + PresentationContextInvalid = 3 + } } #endif diff --git a/Assets/Thirdweb/Core/Scripts/Browser/StandaloneBrowser.cs b/Assets/Thirdweb/Core/Scripts/Browser/StandaloneBrowser.cs index 978a17c4..acc16ad7 100644 --- a/Assets/Thirdweb/Core/Scripts/Browser/StandaloneBrowser.cs +++ b/Assets/Thirdweb/Core/Scripts/Browser/StandaloneBrowser.cs @@ -1,4 +1,4 @@ -#if !UNITY_IOS && !UNITY_ANDROID +#if UNITY_EDITOR || (!UNITY_IOS && !UNITY_ANDROID) using System; using System.Net; diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 4c0f6ba8..d76554c2 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -840,6 +840,7 @@ PlayerSettings: Android: 1 Standalone: 1 WebGL: 1 + iPhone: 1 managedStrippingLevel: Android: 4 EmbeddedLinux: 1 @@ -853,7 +854,7 @@ PlayerSettings: WebGL: 4 Windows Store Apps: 1 XboxOne: 1 - iPhone: 1 + iPhone: 4 tvOS: 1 incrementalIl2cppBuild: {} suppressCommonWarnings: 1