Files
firka/firka/lib/ui/phone/widgets/login_webview.dart

272 lines
9.1 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:firka/data/models/app_settings_model.dart';
import 'package:firka/services/live_activity_service.dart';
import 'package:firka/app/app_state.dart';
import 'package:flutter/material.dart';
import 'package:isar_community/isar.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:firka/services/watch_sync_helper.dart';
import 'package:firka/api/consts.dart';
import 'package:firka/api/token_grant.dart';
import 'package:firka/data/models/token_model.dart';
import 'package:firka/app/initialization_screen.dart';
import 'package:firka/core/state/firka_state.dart';
import 'package:firka/core/settings.dart';
import 'package:firka/ui/theme/style.dart';
class LoginWebviewWidget extends StatefulWidget {
final AppInitialization data;
final String? username;
final String? schoolId;
const LoginWebviewWidget(
this.data, {
super.key,
this.username,
this.schoolId,
});
@override
State<LoginWebviewWidget> createState() => _LoginWebviewWidgetState();
}
class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
with TickerProviderStateMixin {
late WebViewController _webViewController;
bool _isLoading = true;
AnimationController? _fadeAnimationController;
Animation<double>? _fadeAnimation;
@override
void initState() {
super.initState();
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_fadeAnimationController!);
var loginUrl = KretaEndpoints.kretaLoginUrl;
if (widget.username != null && widget.schoolId != null) {
loginUrl = KretaEndpoints.kretaLoginUrlRefresh(
widget.username!,
widget.schoolId!,
);
}
logger.info("Using loginUrl: $loginUrl");
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(loginUrl))
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
Timer(const Duration(milliseconds: 500), () {
if (mounted) {
setState(() {
_isLoading = false;
});
_fadeAnimationController?.forward().then((_) {
_fadeAnimationController?.reset();
});
}
});
},
onPageStarted: (String url) {
setState(() {
_isLoading = true;
});
_fadeAnimationController?.reset();
},
onNavigationRequest: (NavigationRequest request) async {
var uri = Uri.parse(request.url);
if (uri.path == "/ellenorzo-student/prod/oauthredirect") {
var code = uri.queryParameters["code"]!;
try {
var isar = widget.data.isar;
var resp = await getAccessToken(code);
logger.info("getAccessToken(): $resp");
var tokenModel = TokenModel.fromResp(resp);
final accountPicker =
(widget.data.settings.group(
"profile_settings",
)["e_kreta_account_picker"]
as SettingsKretenAccountPicker);
var tokenId = 0;
var om = 0;
await isar.writeTxn(() async {
om = await isar.tokenModels.put(tokenModel);
});
widget.data.tokens = await isar.tokenModels.where().findAll();
for (var i = 0; i < widget.data.tokens.length; i++) {
if (widget.data.tokens[i].studentIdNorm == om) {
tokenId = i;
break;
}
}
await isar.writeTxn(() async {
accountPicker.accountIndex = tokenId;
await accountPicker.save(widget.data.isar.appSettingsModels);
});
await accountPicker.postUpdate();
if (Platform.isIOS) {
final watchInstalled =
await WatchSyncHelper.isWatchAppInstalled();
if (watchInstalled) {
try {
await WatchSyncHelper.saveTokenToiCloud(tokenModel);
} catch (_) {}
try {
await WatchSyncHelper.sendTokenToWatch();
} catch (_) {
// Watch may be unavailable, ignore
}
}
}
if (!mounted) return NavigationDecision.prevent;
widget.data.reauthCubit?.clear();
if (Platform.isIOS) {
LiveActivityService.clearTokenExpiration();
}
runApp(InitializationScreen());
} catch (ex) {
if (ex is Error) {
logger.shout(
"oauthredirect failed:",
ex.toString(),
ex.stackTrace,
);
} else {
logger.shout("oauthredirect failed:", ex.toString());
}
appRouter?.go('/error', extra: ex.toString());
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
);
}
@override
void dispose() {
_fadeAnimationController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
color: appStyle.colors.card,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: FractionallySizedBox(
heightFactor: 0.90,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.secondary.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.all(Radius.circular(2)),
),
width: 40,
height: 4,
),
),
],
),
Container(
height: MediaQuery.of(context).size.height * 0.8,
// Adjust height for content
margin: const EdgeInsets.symmetric(horizontal: 16),
// Add ClipRRect for circular edges
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
WebViewWidget(controller: _webViewController),
if (_fadeAnimationController != null &&
_fadeAnimation != null)
IgnorePointer(
ignoring: !_isLoading,
child: AnimatedBuilder(
animation: _fadeAnimationController!,
builder: (context, child) => AnimatedOpacity(
opacity: _isLoading
? 1.0
: _fadeAnimationController!.isAnimating
? _fadeAnimation!.value
: 0.0,
duration: const Duration(milliseconds: 500),
child: Container(
color: appStyle.colors.background,
child: Center(
child: SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor:
AlwaysStoppedAnimation<Color>(
appStyle.colors.accent,
),
),
),
),
),
),
),
),
],
),
),
),
],
),
),
),
),
);
}
}