225 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:io';
 | |
| 
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 | |
| import 'package:flutter_riverpod/flutter_riverpod.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:island/pods/config.dart';
 | |
| import 'package:island/pods/network.dart';
 | |
| import 'package:island/services/udid.dart';
 | |
| import 'package:island/widgets/alert.dart';
 | |
| import 'package:island/widgets/app_scaffold.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| class OidcScreen extends ConsumerStatefulWidget {
 | |
|   final String provider;
 | |
|   final String? title;
 | |
| 
 | |
|   const OidcScreen({super.key, required this.provider, this.title});
 | |
| 
 | |
|   @override
 | |
|   ConsumerState<OidcScreen> createState() => _OidcScreenState();
 | |
| }
 | |
| 
 | |
| class _OidcScreenState extends ConsumerState<OidcScreen> {
 | |
|   String? authToken;
 | |
|   String? currentUrl;
 | |
|   final TextEditingController _urlController = TextEditingController();
 | |
|   bool _isLoading = true;
 | |
|   late Future<String> _deviceIdFuture;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _deviceIdFuture = getUdid();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _urlController.dispose();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final serverUrl = ref.watch(serverUrlProvider);
 | |
|     final token = ref.watch(tokenProvider);
 | |
| 
 | |
|     return AppScaffold(
 | |
|       appBar: AppBar(
 | |
|         title: widget.title != null ? Text(widget.title!) : Text('login').tr(),
 | |
|       ),
 | |
|       body: FutureBuilder<String>(
 | |
|         future: _deviceIdFuture,
 | |
|         builder: (context, snapshot) {
 | |
|           if (snapshot.connectionState == ConnectionState.waiting) {
 | |
|             return const Center(child: CircularProgressIndicator());
 | |
|           }
 | |
| 
 | |
|           if (snapshot.hasError) {
 | |
|             return Center(child: Text('somethingWentWrong').tr());
 | |
|           }
 | |
| 
 | |
|           final deviceId = snapshot.data!;
 | |
| 
 | |
|           return Column(
 | |
|             children: [
 | |
|               Expanded(
 | |
|                 child: InAppWebView(
 | |
|                   initialSettings: InAppWebViewSettings(
 | |
|                     userAgent:
 | |
|                         kIsWeb
 | |
|                             ? null
 | |
|                             : Platform.isIOS
 | |
|                             ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'
 | |
|                             : Platform.isAndroid
 | |
|                             ? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
 | |
|                             : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
 | |
|                   ),
 | |
|                   initialUrlRequest: URLRequest(
 | |
|                     url: WebUri(
 | |
|                       '$serverUrl/pass/auth/login/${widget.provider}',
 | |
|                     ),
 | |
|                     headers: {
 | |
|                       if (token?.token.isNotEmpty ?? false)
 | |
|                         'Authorization': 'AtField ${token!.token}',
 | |
|                       'X-Device-Id': deviceId,
 | |
|                     },
 | |
|                   ),
 | |
|                   onWebViewCreated: (controller) {
 | |
|                     // Register a handler to receive the token from JavaScript
 | |
|                     controller.addJavaScriptHandler(
 | |
|                       handlerName: 'tokenHandler',
 | |
|                       callback: (args) {
 | |
|                         // args[0] will be the token string
 | |
|                         if (args.isNotEmpty && args[0] is String) {
 | |
|                           setState(() {
 | |
|                             authToken = args[0];
 | |
|                           });
 | |
| 
 | |
|                           // Return the token and close the webview
 | |
|                           Navigator.of(context).pop(authToken);
 | |
|                         }
 | |
|                       },
 | |
|                     );
 | |
|                   },
 | |
|                   shouldOverrideUrlLoading: (
 | |
|                     controller,
 | |
|                     navigationAction,
 | |
|                   ) async {
 | |
|                     final url = navigationAction.request.url;
 | |
|                     if (url != null) {
 | |
|                       setState(() {
 | |
|                         currentUrl = url.toString();
 | |
|                         _urlController.text = currentUrl ?? '';
 | |
|                         _isLoading = true;
 | |
|                       });
 | |
| 
 | |
|                       final path = url.path;
 | |
|                       final queryParams = url.queryParameters;
 | |
| 
 | |
|                       // Check if we're on the token page
 | |
|                       if (path.endsWith('/auth/callback')) {
 | |
|                         // Extract token from URL
 | |
|                         final challenge = queryParams['challenge'];
 | |
|                         // Return the token and close the webview
 | |
|                         Navigator.of(context).pop(challenge);
 | |
|                         return NavigationActionPolicy.CANCEL;
 | |
|                       }
 | |
|                     }
 | |
|                     return NavigationActionPolicy.ALLOW;
 | |
|                   },
 | |
|                   onUpdateVisitedHistory: (controller, url, androidIsReload) {
 | |
|                     if (url != null) {
 | |
|                       setState(() {
 | |
|                         currentUrl = url.toString();
 | |
|                         _urlController.text = currentUrl ?? '';
 | |
|                       });
 | |
|                     }
 | |
|                   },
 | |
|                   onLoadStop: (controller, url) {
 | |
|                     setState(() {
 | |
|                       _isLoading = false;
 | |
|                     });
 | |
|                   },
 | |
|                   onLoadStart: (controller, url) {
 | |
|                     setState(() {
 | |
|                       _isLoading = true;
 | |
|                     });
 | |
|                   },
 | |
|                   onLoadError: (controller, url, code, message) {
 | |
|                     setState(() {
 | |
|                       _isLoading = false;
 | |
|                     });
 | |
|                   },
 | |
|                 ),
 | |
|               ),
 | |
|               // Loading progress indicator
 | |
|               if (_isLoading)
 | |
|                 LinearProgressIndicator(
 | |
|                   color: Theme.of(context).colorScheme.primary,
 | |
|                   backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
 | |
|                   borderRadius: BorderRadius.zero,
 | |
|                   stopIndicatorRadius: 0,
 | |
|                   minHeight: 2,
 | |
|                 )
 | |
|               else
 | |
|                 ColoredBox(
 | |
|                   color: Theme.of(context).colorScheme.surfaceVariant,
 | |
|                 ).height(2),
 | |
|               // Debug location bar (only visible in debug mode)
 | |
|               Container(
 | |
|                 padding: EdgeInsets.only(
 | |
|                   left: 16,
 | |
|                   right: 0,
 | |
|                   bottom: MediaQuery.of(context).padding.bottom + 8,
 | |
|                   top: 8,
 | |
|                 ),
 | |
|                 color: Theme.of(context).colorScheme.surface,
 | |
|                 child: Row(
 | |
|                   children: [
 | |
|                     Expanded(
 | |
|                       child: TextField(
 | |
|                         controller: _urlController,
 | |
|                         decoration: InputDecoration(
 | |
|                           isDense: true,
 | |
|                           contentPadding: const EdgeInsets.symmetric(
 | |
|                             horizontal: 8,
 | |
|                             vertical: 8,
 | |
|                           ),
 | |
|                           border: OutlineInputBorder(
 | |
|                             borderRadius: BorderRadius.circular(4),
 | |
|                           ),
 | |
|                           hintText: 'URL',
 | |
|                         ),
 | |
|                         style: const TextStyle(fontSize: 12),
 | |
|                         readOnly: true,
 | |
|                       ),
 | |
|                     ),
 | |
|                     const Gap(4),
 | |
|                     IconButton(
 | |
|                       icon: const Icon(Icons.copy, size: 20),
 | |
|                       padding: const EdgeInsets.all(4),
 | |
|                       constraints: const BoxConstraints(),
 | |
|                       onPressed: () {
 | |
|                         if (currentUrl != null) {
 | |
|                           Clipboard.setData(ClipboardData(text: currentUrl!));
 | |
|                           showSnackBar('copyToClipboard'.tr());
 | |
|                         }
 | |
|                       },
 | |
|                     ),
 | |
|                     const Gap(8),
 | |
|                   ],
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           );
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |