Skip to content

Commit

Permalink
Merge pull request supabase#136 from dshukertjr/feat/desktop_support
Browse files Browse the repository at this point in the history
feat: add Mac OS and Windows support for deeplinks
  • Loading branch information
dshukertjr committed Jul 26, 2022
2 parents 21d2270 + fa7a736 commit 50d532e
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 64 deletions.
228 changes: 174 additions & 54 deletions README.md
Expand Up @@ -33,8 +33,8 @@ Supabase is an open source Firebase alternative. We are a service to:
| Web ||||||
| Android ||||||
| iOS ||||||
| macOS || ||||
| Windows || ||||
| macOS || ||||
| Windows || ||||
| Linux || ||||

## Getting Started
Expand Down Expand Up @@ -279,7 +279,7 @@ class _MyWidgetState extends State<MyWidget> {

## Authentication

Using authentication can be done easily. Using this package automatically persists the auth state on local storage.
Using this package automatically persists the auth state on local storage.
It also helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter...


Expand Down Expand Up @@ -383,68 +383,188 @@ The redirect callback url should have this format `[YOUR_SCHEME]://[YOUR_AUTH_HO

Follow the guide https://supabase.io/docs/guides/auth#third-party-logins

### For Android
#### For Android

Deep Links can have any custom scheme. The downside is that any app can claim a scheme, so make sure yours are as unique as possible, eg. `HST0000001://host.com`.
<details>
<summary>How to setup</summary>

```xml
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->

<!-- Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="[YOUR_SCHEME]"
android:host="[YOUR_HOST]" />
</intent-filter>
</activity>
</application>
</manifest>
```
Deep Links can have any custom scheme. The downside is that any app can claim a scheme, so make sure yours are as unique as possible, eg. `HST0000001://host.com`.

The `android:host` attribute is optional for Deep Links.
```xml
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->

For more info: https://developer.android.com/training/app-links/deep-linking
<!-- Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="[YOUR_SCHEME]"
android:host="[YOUR_HOST]" />
</intent-filter>
</activity>
</application>
</manifest>
```

### For iOS
The `android:host` attribute is optional for Deep Links.

Custom URL schemes can have... any custom scheme and there is no host specificity, nor entitlements or a hosted file. The downside is that any app can claim any scheme, so make sure yours is as unique as possible, eg. `hst0000001` or `myIncrediblyAwesomeScheme`.
For more info: https://developer.android.com/training/app-links/deep-linking
</details>

For **Custom URL schemes** you need to declare the scheme in
`ios/Runner/Info.plist` (or through Xcode's Target Info editor,
under URL Types):
#### For iOS

```xml
<!-- ... other tags -->
<plist>
<dict>
<!-- ... other tags -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>[YOUR_SCHEME]</string>
</array>
</dict>
</array>
<details>
<summary>How to setup</summary>

Custom URL schemes can have... any custom scheme and there is no host specificity, nor entitlements or a hosted file. The downside is that any app can claim any scheme, so make sure yours is as unique as possible, eg. `hst0000001` or `myIncrediblyAwesomeScheme`.

For **Custom URL schemes** you need to declare the scheme in
`ios/Runner/Info.plist` (or through Xcode's Target Info editor,
under URL Types):

```xml
<!-- ... other tags -->
</dict>
</plist>
```
<plist>
<dict>
<!-- ... other tags -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>[YOUR_SCHEME]</string>
</array>
</dict>
</array>
<!-- ... other tags -->
</dict>
</plist>
```

This allows for your app to be started from `YOUR_SCHEME://ANYTHING` links.

For more info: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app

</details>

#### For Windows

<details>
<summary>How to setup</summary>

Setting up deeplinks in Windows has few more steps than other platforms. [Learn more](https://pub.dev/packages/app_links#windows)

Declare this method in <PROJECT_DIR>\windows\runner\win32_window.h
```cpp
// Dispatches link if any.
// This method enables our app to be with a single instance too.
// This is optional but mandatory if you want to catch further links in same app.
bool SendAppLinkToInstance(const std::wstring& title);
```
Add this inclusion at the top of <PROJECT_DIR>\windows\runner\win32_window.cpp
```cpp
#include "app_links_windows/app_links_windows_plugin.h"
```

Add this method in <PROJECT_DIR>\windows\runner\win32_window.cpp
```cpp
bool Win32Window::SendAppLinkToInstance(const std::wstring& title) {
// Find our exact window
HWND hwnd = ::FindWindow(kWindowClassName, title.c_str());

if (hwnd) {
// Dispatch new link to current window
SendAppLink(hwnd);

// (Optional) Restore our window to front in same state
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
GetWindowPlacement(hwnd, &place);
switch(place.showCmd) {
case SW_SHOWMAXIMIZED:
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
break;
case SW_SHOWMINIMIZED:
ShowWindow(hwnd, SW_RESTORE);
break;
default:
ShowWindow(hwnd, SW_NORMAL);
break;
}
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
SetForegroundWindow(hwnd);
// END Restore

// Window has been found, don't create another one.
return true;
}

return false;
}
```
Add the call to the previous method in `CreateAndShow`
```cpp
bool Win32Window::CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size) {
if (SendAppLinkToInstance(title)) {
return false;
}
This allows for your app to be started from `YOUR_SCHEME://ANYTHING` links.
...
```

For more info: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app
At this point, you can register your own scheme.
On Windows, URL protocols are setup in the Windows registry.

This package won't do it for you.

You can achieve it with [url_protocol](https://pub.dev/packages/url_protocol) inside you app.

The most relevant solution is to include those registry modifications into your installer to allow the unregistration.

</details>

#### For Mac OS

<details>
<summary>How to setup</summary>

Add this XML chapter in your macos/Runner/Info.plist inside <plist version="1.0"><dict> chapter:

```xml
<!-- ... other tags -->
<plist version="1.0">
<dict>
<!-- ... other tags -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<!-- abstract name for this URL type (you can leave it blank) -->
<string>sample_name</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- your schemes -->
<string>sample</string>
</array>
</dict>
</array>
<!-- ... other tags -->
</dict>
</plist>
```

</details>

---

Expand Down
13 changes: 8 additions & 5 deletions lib/src/supabase_auth.dart
@@ -1,9 +1,9 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uni_links/uni_links.dart';

import 'package:url_launcher/url_launcher.dart';

Expand All @@ -28,8 +28,9 @@ class SupabaseAuth with WidgetsBindingObserver {
Future<String?> get accessToken => _localStorage.accessToken();

/// Returns when the initial session recovery is done.
/// Can be used to determine whether a user is signed in or not upon
/// initial app launch.
///
/// Can be used to determine whether a user is signed in upon initial
/// app launch.
Future<Session?> get initialSession => _initialSessionCompleter.future;
final Completer<Session?> _initialSessionCompleter = Completer();

Expand All @@ -44,6 +45,8 @@ class SupabaseAuth with WidgetsBindingObserver {

StreamSubscription<Uri?>? _deeplinkSubscription;

final _appLinks = AppLinks();

/// Listen to auth change events.
///
/// ```dart
Expand Down Expand Up @@ -218,7 +221,7 @@ class SupabaseAuth with WidgetsBindingObserver {
if (!kIsWeb) {
// It will handle app links while the app is already started - be it in
// the foreground or in the background.
_deeplinkSubscription = uriLinkStream.listen(
_deeplinkSubscription = _appLinks.uriLinkStream.listen(
(Uri? uri) {
if (uri != null) {
_handleDeeplink(uri);
Expand All @@ -243,7 +246,7 @@ class SupabaseAuth with WidgetsBindingObserver {
_initialDeeplinkIsHandled = true;

try {
final uri = await getInitialUri();
final uri = await _appLinks.getInitialAppLink();
if (uri != null) {
_handleDeeplink(uri);
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Expand Up @@ -9,12 +9,12 @@ environment:
flutter: '>=2.5.0'

dependencies:
app_links: ^3.2.0
flutter:
sdk: flutter
hive: ^2.2.1
hive_flutter: ^1.1.0
supabase: ^0.3.6
uni_links: ^0.5.1
url_launcher: ^6.1.2

dev_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion test/supabase_flutter_test.dart
Expand Up @@ -8,7 +8,7 @@ void main() {
const supabaseKey = '';

setUpAll(() async {
mockUniLink();
mockAppLink();
// Initialize the Supabase singleton
await Supabase.initialize(
url: supabaseUrl,
Expand Down
2 changes: 1 addition & 1 deletion test/widget_test.dart
Expand Up @@ -10,7 +10,7 @@ void main() {
const supabaseKey = '';

setUpAll(() async {
mockUniLink();
mockAppLink();

// Initialize the Supabase singleton
await Supabase.initialize(
Expand Down
4 changes: 2 additions & 2 deletions test/widget_test_stubs.dart
Expand Up @@ -53,8 +53,8 @@ class MockLocalStorage extends LocalStorage {
}

// Register the mock handler for uni_links
void mockUniLink() {
const channel = MethodChannel('uni_links/messages');
void mockAppLink() {
const channel = MethodChannel('com.llfbandit.app_links/messages');
TestWidgetsFlutterBinding.ensureInitialized();

TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger
Expand Down

0 comments on commit 50d532e

Please sign in to comment.