2022-06-24 02:47:33 -04:00
import ' dart:async ' ;
2023-01-18 05:34:54 -05:00
import ' package:flutter/material.dart ' ;
2021-08-21 22:38:25 -04:00
import ' package:flutter_secure_storage/flutter_secure_storage.dart ' ;
2022-08-31 14:11:45 -04:00
import ' package:frosty/apis/twitch_api.dart ' ;
import ' package:frosty/constants.dart ' ;
2022-06-24 02:47:33 -04:00
import ' package:frosty/main.dart ' ;
2022-08-31 14:11:45 -04:00
import ' package:frosty/screens/settings/stores/user_store.dart ' ;
2023-01-18 05:34:54 -05:00
import ' package:frosty/widgets/button.dart ' ;
import ' package:frosty/widgets/dialog.dart ' ;
2021-11-14 23:18:43 -05:00
import ' package:mobx/mobx.dart ' ;
2022-06-24 02:47:33 -04:00
import ' package:webview_flutter/webview_flutter.dart ' ;
2021-08-21 22:38:25 -04:00
2021-10-01 00:08:19 -04:00
part ' auth_store.g.dart ' ;
2022-05-12 04:58:03 -04:00
class AuthStore = AuthBase with _ $AuthStore ;
2021-09-06 18:59:33 -04:00
2022-05-12 04:58:03 -04:00
abstract class AuthBase with Store {
2021-10-05 03:08:38 -04:00
/// Secure storage to store tokens.
2022-01-25 01:02:08 -05:00
static const _storage = FlutterSecureStorage ( ) ;
2021-09-06 18:59:33 -04:00
2022-01-25 02:07:08 -05:00
/// The shared_preferences key for the default token.
static const _defaultTokenKey = ' default_token ' ;
/// The shared_preferences key for the user token.
static const _userTokenKey = ' user_token ' ;
2022-06-24 02:47:33 -04:00
/// The Twitch API service for making requests.
2022-01-14 14:56:54 -05:00
final TwitchApi twitchApi ;
2021-10-16 04:01:09 -04:00
/// Whether the token is valid or not.
var _tokenIsValid = false ;
2022-06-24 02:47:33 -04:00
/// The MobX store containing information relevant to the current user.
2022-01-14 14:56:54 -05:00
final UserStore user ;
2021-12-21 23:20:38 -05:00
2022-06-24 02:47:33 -04:00
/// The user token used to authenticate with the Twitch API.
2021-10-17 16:26:02 -04:00
@ readonly
String ? _token ;
2021-10-05 03:08:38 -04:00
/// Whether the user is logged in or not.
2021-10-11 23:01:40 -04:00
@ readonly
2021-10-03 22:13:36 -04:00
var _isLoggedIn = false ;
2021-08-21 22:38:25 -04:00
2021-10-05 03:08:38 -04:00
/// Authentication headers for Twitch API requests.
2021-10-03 22:13:36 -04:00
@ computed
2023-04-29 11:34:28 -04:00
Map < String , String > get headersTwitch = >
{ ' Authorization ' : ' Bearer $ _token ' , ' Client-Id ' : clientId } ;
2021-08-21 22:38:25 -04:00
2022-06-24 02:47:33 -04:00
/// Error flag that will be non-null and contain an error message if login failed.
2022-01-15 16:51:11 -05:00
@ readonly
String ? _error ;
2022-06-24 02:47:33 -04:00
/// Navigation handler for the login webview. Fires on every navigation request (whenever the URL changes).
2023-04-29 11:34:28 -04:00
FutureOr < NavigationDecision > handleNavigation (
2023-05-30 21:10:04 -04:00
{ required NavigationRequest request , Widget ? routeAfter } ) {
2022-06-24 02:47:33 -04:00
// Check if the URL is the redirect URI.
2023-05-30 21:10:04 -04:00
if ( request . url . startsWith ( ' https://twitch.tv/login ' ) ) {
2022-06-24 02:47:33 -04:00
// Extract the token from the query parameters.
2023-05-30 21:10:04 -04:00
final uri = Uri . parse ( request . url . replaceFirst ( ' # ' , ' ? ' ) ) ;
2022-06-24 02:47:33 -04:00
final token = uri . queryParameters [ ' access_token ' ] ;
// Login with the provided token.
if ( token ! = null ) login ( token: token ) ;
}
// Check if the the URL has been redirected to "https://www.twitch.tv/?no-reload=true".
// When redirected to the redirect_uri, there will be another redirect to "https://www.twitch.tv/?no-reload=true".
// Checking for this will ensure that the user has automatically logged in to Twitch on the WebView itself.
2023-05-30 21:10:04 -04:00
if ( request . url = = ' https://www.twitch.tv/?no-reload=true ' ) {
2023-01-19 02:27:51 -05:00
if ( routeAfter ! = null ) {
navigatorKey . currentState ? . pop ( ) ;
2023-04-29 11:34:28 -04:00
navigatorKey . currentState
? . push ( MaterialPageRoute ( builder: ( context ) = > routeAfter ) ) ;
2023-01-19 02:27:51 -05:00
} else {
// Pop twice, once to dismiss the WebView and again to dismiss the Login dialog.
navigatorKey . currentState ? . pop ( ) ;
navigatorKey . currentState ? . pop ( ) ;
}
2022-06-24 02:47:33 -04:00
}
// Always allow navigation to the next URL.
return NavigationDecision . navigate ;
}
2023-05-30 21:10:04 -04:00
WebViewController createAuthWebViewController ( { Widget ? routeAfter } ) {
return WebViewController ( )
. . setJavaScriptMode ( JavaScriptMode . unrestricted )
. . setNavigationDelegate (
NavigationDelegate (
onNavigationRequest: ( request ) = >
handleNavigation ( request: request , routeAfter: routeAfter ) ,
) ,
)
. . loadRequest (
Uri (
scheme: ' https ' ,
host: ' id.twitch.tv ' ,
path: ' /oauth2/authorize ' ,
queryParameters: {
' client_id ' : clientId ,
' redirect_uri ' : ' https://twitch.tv/login ' ,
' response_type ' : ' token ' ,
' scope ' :
' chat:read chat:edit user:read:follows user:read:blocked_users user:manage:blocked_users ' ,
' force_verify ' : ' true ' ,
} ,
) ,
) ;
}
2023-01-18 05:34:54 -05:00
/// Shows a dialog verifying that the user is sure they want to block/unblock the target user.
Future < void > showBlockDialog (
BuildContext context , {
required String targetUser ,
required String targetUserId ,
} ) {
2023-04-29 11:34:28 -04:00
final isBlocked = user . blockedUsers
. where ( ( blockedUser ) = > blockedUser . userId = = targetUserId )
. isNotEmpty ;
2023-01-18 05:34:54 -05:00
final title = isBlocked ? ' Unblock $ targetUser ' : ' Block $ targetUser ' ;
final message =
' Are you sure you want to ${ isBlocked ? ' unblock $ targetUser ? ' : ' block $ targetUser ? This will remove them from channel lists, search results, and chat messages. ' } ' ;
void onPressed ( ) {
if ( isBlocked ) {
user . unblock ( targetId: targetUserId , headers: headersTwitch ) ;
} else {
user . block (
targetId: targetUserId ,
displayName: targetUser ,
headers: headersTwitch ,
) ;
}
Navigator . pop ( context ) ;
}
return showDialog (
context: context ,
builder: ( context ) = > FrostyDialog (
title: title ,
message: message ,
actions: [
Button (
onPressed: onPressed ,
child: const Text ( ' Yes ' ) ,
) ,
Button (
onPressed: Navigator . of ( context ) . pop ,
color: Colors . grey ,
child: const Text ( ' Cancel ' ) ,
) ,
] ,
) ,
) ;
}
2022-05-12 04:58:03 -04:00
AuthBase ( { required this . twitchApi } ) : user = UserStore ( twitchApi: twitchApi ) ;
2022-01-14 14:56:54 -05:00
2021-08-21 22:38:25 -04:00
/// Initialize by retrieving a token if it does not already exist.
2021-10-03 22:13:36 -04:00
@ action
2021-08-24 09:58:31 -04:00
Future < void > init ( ) async {
2022-01-15 16:51:11 -05:00
try {
// Read and set the currently stored user token, if any.
2022-01-25 02:07:08 -05:00
_token = await _storage . read ( key: _userTokenKey ) ;
2021-08-25 00:43:51 -04:00
2022-01-15 16:51:11 -05:00
// If the token does not exist, get the default token.
// Otherwise, log in.
2021-10-03 22:13:36 -04:00
if ( _token = = null ) {
2022-01-15 16:51:11 -05:00
// Retrieve the currently stored default token if it exists.
2022-01-25 02:07:08 -05:00
_token = await _storage . read ( key: _defaultTokenKey ) ;
2022-05-03 01:44:01 -04:00
// If the token does not exist or is invalid, get a new token and store it.
if ( _token = = null | | ! await twitchApi . validateToken ( token: _token ! ) ) {
2022-01-15 16:51:11 -05:00
_token = await twitchApi . getDefaultToken ( ) ;
2022-01-25 02:07:08 -05:00
await _storage . write ( key: _defaultTokenKey , value: _token ) ;
2022-01-15 16:51:11 -05:00
}
} else {
2022-04-06 03:04:11 -04:00
// Validate the existing token.
_tokenIsValid = await twitchApi . validateToken ( token: _token ! ) ;
// If the token is invalid, logout.
if ( ! _tokenIsValid ) return await logout ( ) ;
2022-01-15 16:51:11 -05:00
// Initialize the user store.
await user . init ( headers: headersTwitch ) ;
if ( user . details ! = null ) _isLoggedIn = true ;
2021-10-03 22:13:36 -04:00
}
2021-12-11 00:45:44 -05:00
2022-01-15 16:51:11 -05:00
_error = null ;
2023-04-30 00:07:43 -04:00
} catch ( e ) {
2022-01-15 16:51:11 -05:00
debugPrint ( e . toString ( ) ) ;
_error = e . toString ( ) ;
2021-08-21 22:38:25 -04:00
}
2021-08-25 00:43:51 -04:00
}
2022-06-24 02:47:33 -04:00
/// Logs in the user with the provided [token] and updates fields accordingly upon successful login.
2021-10-03 22:13:36 -04:00
@ action
2022-06-24 02:47:33 -04:00
Future < void > login ( { required String token } ) async {
2021-08-25 00:43:51 -04:00
try {
2022-06-24 02:47:33 -04:00
// Validate the custom token.
_tokenIsValid = await twitchApi . validateToken ( token: token ) ;
if ( ! _tokenIsValid ) return ;
2022-02-01 23:34:55 -05:00
2022-06-24 02:47:33 -04:00
// Replace the current default token with the new custom token.
_token = token ;
2021-08-25 00:43:51 -04:00
2021-10-05 03:08:38 -04:00
// Store the user token.
2022-06-24 02:47:33 -04:00
await _storage . write ( key: _userTokenKey , value: token ) ;
2021-08-25 00:43:51 -04:00
2021-12-21 23:20:38 -05:00
// Initialize the user with the new token.
2021-12-29 18:11:39 -05:00
await user . init ( headers: headersTwitch ) ;
2021-12-11 00:45:44 -05:00
2021-10-05 03:08:38 -04:00
// Set the login status to logged in.
2022-05-03 01:44:01 -04:00
if ( user . details ! = null ) _isLoggedIn = true ;
2023-04-30 00:07:43 -04:00
} catch ( e ) {
2022-08-01 17:55:12 -04:00
debugPrint ( ' Login failed due to $ e ' ) ;
2021-08-25 00:43:51 -04:00
}
}
2021-10-05 03:08:38 -04:00
/// Logs out the current user and updates fields accordingly.
2021-10-03 22:13:36 -04:00
@ action
Future < void > logout ( ) async {
2022-01-15 16:51:11 -05:00
try {
2022-02-01 23:34:55 -05:00
// Delete the existing user token.
2022-01-25 02:07:08 -05:00
await _storage . delete ( key: _userTokenKey ) ;
2022-01-15 16:51:11 -05:00
_token = null ;
2021-10-03 22:13:36 -04:00
2022-01-15 16:51:11 -05:00
// Clear the user info.
user . dispose ( ) ;
2021-10-05 00:44:46 -04:00
2022-01-15 16:51:11 -05:00
// If the default token already exists, set it.
2022-01-25 02:07:08 -05:00
_token = await _storage . read ( key: _defaultTokenKey ) ;
2021-10-17 16:26:02 -04:00
2022-05-03 01:44:01 -04:00
// If the default token does not already exist or it's invalid, get the new default token and store it.
if ( _token = = null | | ! await twitchApi . validateToken ( token: _token ! ) ) {
2022-01-14 14:56:54 -05:00
_token = await twitchApi . getDefaultToken ( ) ;
2022-01-25 02:07:08 -05:00
await _storage . write ( key: _defaultTokenKey , value: _token ) ;
2021-10-17 16:26:02 -04:00
}
2022-01-25 01:02:08 -05:00
2022-01-18 21:41:29 -05:00
// Set the login status to logged out.
_isLoggedIn = false ;
debugPrint ( ' Successfully logged out ' ) ;
2023-04-30 00:07:43 -04:00
} catch ( e ) {
2022-01-15 16:51:11 -05:00
debugPrint ( e . toString ( ) ) ;
2021-10-17 16:26:02 -04:00
}
2021-08-21 22:38:25 -04:00
}
}