Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions lib/ui/viewmodels/animes/followed_anime_controller.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:application/ui/viewmodels/generic_controller.dart';
import 'package:application/ui/viewmodels/member_controller.dart';
import 'package:application/ui/viewmodels/auth_viewmodel.dart';
import 'package:application/data/models/anime_dto.dart';
import 'package:application/data/models/pageable_dto.dart';
import 'package:application/core/network/api_client.dart';
Expand All @@ -11,7 +11,6 @@ class FollowedAnimeController extends GenericController<AnimeDto> {
: _client = client ?? const ApiClient();
static final FollowedAnimeController instance = FollowedAnimeController();
final ApiClient _client;
bool _isRetry = false;

int get limit =>
wb.WidgetBuilder.getDeviceType() == wb.DeviceType.mobile ? 9 : 24;
Expand All @@ -25,25 +24,26 @@ class FollowedAnimeController extends GenericController<AnimeDto> {
}

@override
Future<Pair<Iterable<AnimeDto>, int>> fetchItems() async {
final ApiResult<PageableDto> result = await _client.getPage(
Future<Pair<Iterable<AnimeDto>, int>> fetchItems() =>
_fetchItems(isRetry: false);

Future<Pair<Iterable<AnimeDto>, int>> _fetchItems({
required final bool isRetry,
}) async {
final result = await _client.getPage(
'/v1/animes',
query: <String, Object>{'page': page, 'limit': limit},
token: MemberController.instance.member?.token,
token: AuthViewModel.instance.member?.token,
);

if (result is ApiFailure<PageableDto> && result.statusCode == 401) {
if (_isRetry) {
if (isRetry) {
throw Exception('Unauthorized after retry');
}

_isRetry = true;
await MemberController.instance.login();
return fetchItems();
await AuthViewModel.instance.login();
return _fetchItems(isRetry: true);
}

_isRetry = false;

return switch (result) {
ApiSuccess<PageableDto>(:final data) => Pair<Iterable<AnimeDto>, int>(
data.data.map(
Expand Down
24 changes: 12 additions & 12 deletions lib/ui/viewmodels/animes/missed_anime_controller.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:application/ui/viewmodels/generic_controller.dart';
import 'package:application/ui/viewmodels/member_controller.dart';
import 'package:application/ui/viewmodels/auth_viewmodel.dart';
import 'package:application/data/models/missed_anime_dto.dart';
import 'package:application/data/models/pageable_dto.dart';
import 'package:application/core/constants/constant.dart';
Expand All @@ -12,7 +12,6 @@ class MissedAnimeController extends GenericController<MissedAnimeDto> {
: _client = client ?? const ApiClient();
static final MissedAnimeController instance = MissedAnimeController();
final ApiClient _client;
bool _isRetry = false;

int get limit =>
Constant.isAndroidOrIOS &&
Expand All @@ -29,25 +28,26 @@ class MissedAnimeController extends GenericController<MissedAnimeDto> {
}

@override
Future<Pair<Iterable<MissedAnimeDto>, int>> fetchItems() async {
final ApiResult<PageableDto> result = await _client.getPage(
Future<Pair<Iterable<MissedAnimeDto>, int>> fetchItems() =>
_fetchItems(isRetry: false);

Future<Pair<Iterable<MissedAnimeDto>, int>> _fetchItems({
required final bool isRetry,
}) async {
final result = await _client.getPage(
'/v1/animes/missed',
query: <String, Object>{'page': page, 'limit': limit},
token: MemberController.instance.member!.token,
token: AuthViewModel.instance.member!.token,
);

if (result is ApiFailure<PageableDto> && result.statusCode == 401) {
if (_isRetry) {
if (isRetry) {
throw Exception('Unauthorized after retry');
}

_isRetry = true;
await MemberController.instance.login();
return fetchItems();
await AuthViewModel.instance.login();
return _fetchItems(isRetry: true);
}

_isRetry = false;

return switch (result) {
ApiSuccess<PageableDto>(:final data) =>
Pair<Iterable<MissedAnimeDto>, int>(
Expand Down
147 changes: 147 additions & 0 deletions lib/ui/viewmodels/auth_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:application/core/analytics/analytics.dart';
import 'package:application/core/network/api_client.dart';
import 'package:application/core/network/api_result.dart';
import 'package:application/data/models/enums/config_property_key.dart';
import 'package:application/data/models/member_dto.dart';
import 'package:application/ui/viewmodels/shared_preferences_controller.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';

class AuthViewModel {
AuthViewModel({ApiClient? client, Analytics? analytics})
: _client = client ?? const ApiClient(),
_analytics = analytics ?? const Analytics();
static AuthViewModel instance = AuthViewModel();
final ApiClient _client;
final Analytics _analytics;
final StreamController<MemberDto> streamController =
StreamController<MemberDto>.broadcast();
String? identifier;
MemberDto? member;

Future<void> init({final bool afterDelete = false}) async {
identifier =
SharedPreferencesController.instance.getString(
ConfigPropertyKey.identifier,
) ??
await register();

try {
await login();
} on HttpException catch (e) {
debugPrint('Failed to login: $e');

if (!afterDelete) {
// Move the current identifier to old identifier
final String? oldIdentifier = identifier;
await SharedPreferencesController.instance.remove(
ConfigPropertyKey.identifier,
);

await SharedPreferencesController.instance.setString(
ConfigPropertyKey.oldIdentifier,
oldIdentifier!,
);
await init(afterDelete: true);
}
} on TimeoutException catch (e) {
debugPrint('Failed to login: $e');
rethrow;
} on http.ClientException catch (e) {
debugPrint('Failed to login: $e');
rethrow;
}
}

Future<String> register() async {
final ApiResult<http.Response> result = await _client.post(
'/v1/members/register',
);

return switch (result) {
ApiSuccess<http.Response>(:final data) => await _handleRegisterSuccess(
data,
),
ApiFailure<http.Response>(:final error) => throw HttpException(error),
};
}

Future<String> _handleRegisterSuccess(final http.Response response) async {
final String identifier =
(jsonDecode(utf8.decode(response.bodyBytes))
as Map<String, dynamic>)['identifier']
as String;
await SharedPreferencesController.instance.setString(
ConfigPropertyKey.identifier,
identifier,
);
_analytics.logSignUp();
return identifier;
}

String get _device {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return 'android';
case TargetPlatform.iOS:
return 'ios';
case TargetPlatform.macOS:
return 'macos';
case TargetPlatform.windows:
return 'windows';
case TargetPlatform.linux:
return 'linux';
case TargetPlatform.fuchsia:
return 'fuchsia';
}
}

Future<http.Response> testLogin(final String identifier) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();

final ApiResult<http.Response> result = await _client.post(
'/v1/members/login',
headers: <String, String>{
'X-App-Version': '${packageInfo.version}+${packageInfo.buildNumber}',
'X-Device': _device,
'X-Locale': Platform.localeName,
},
body: identifier,
);

return switch (result) {
ApiSuccess<http.Response>(:final data) => data,
ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.notFound =>
throw const HttpException('Failed to login, identifier not found'),
ApiFailure<http.Response>(:final error) => throw http.ClientException(
error,
),
};
}

Future<void> login({final String? identifier}) async {
final http.Response response = await testLogin(
identifier ?? this.identifier!,
);
final Map<String, dynamic> json =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;

if (identifier != null) {
this.identifier = identifier;
await SharedPreferencesController.instance.setString(
ConfigPropertyKey.identifier,
identifier,
);
}

member = MemberDto.fromJson(json);
streamController.add(member!);
_analytics.logLogin();
}
}
3 changes: 3 additions & 0 deletions lib/ui/viewmodels/conflict_email_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ConflictEmailException implements Exception {
const ConflictEmailException();
}
111 changes: 111 additions & 0 deletions lib/ui/viewmodels/email_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'dart:convert';
import 'dart:io';

import 'package:application/core/network/api_client.dart';
import 'package:application/core/network/api_result.dart';
import 'package:application/ui/viewmodels/auth_viewmodel.dart';
import 'package:application/ui/viewmodels/conflict_email_exception.dart';
import 'package:http/http.dart' as http;

class EmailViewModel {
EmailViewModel({ApiClient? client}) : _client = client ?? const ApiClient();

static final EmailViewModel instance = EmailViewModel();
final ApiClient _client;

Future<String> associateEmail(
final String email, {
final bool isRetry = false,
}) async {
final ApiResult<http.Response> result = await _client.post(
'/v1/members/associate-email',
token: AuthViewModel.instance.member!.token,
body: email,
);

switch (result) {
case ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.unauthorized:
if (isRetry) {
throw Exception('Unauthorized after retry');
}
await AuthViewModel.instance.login();
return associateEmail(email, isRetry: true);

case ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.conflict:
throw const ConflictEmailException();

case ApiSuccess<http.Response>(:final data):
return _extractUuid(data);

case ApiFailure<http.Response>(:final error):
throw HttpException(error);
}
}

Future<String> forgotIdentifier(
final String email, {
final bool isRetry = false,
}) async {
final ApiResult<http.Response> result = await _client.post(
'/v1/members/forgot-identifier',
token: AuthViewModel.instance.member!.token,
body: email,
);

switch (result) {
case ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.unauthorized:
if (isRetry) {
throw Exception('Unauthorized after retry');
}
await AuthViewModel.instance.login();
return forgotIdentifier(email, isRetry: true);

case ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.conflict:
throw const ConflictEmailException();

case ApiSuccess<http.Response>(:final data):
return _extractUuid(data);

case ApiFailure<http.Response>(:final error):
throw HttpException(error);
}
}

Future<void> validateAction(
final String uuid,
final String code, {
final bool isRetry = false,
}) async {
final ApiResult<http.Response> result = await _client.post(
'/v1/member-actions/validate?uuid=$uuid',
token: AuthViewModel.instance.member!.token,
body: code,
);

switch (result) {
case ApiFailure<http.Response>(:final statusCode)
when statusCode == HttpStatus.unauthorized:
if (isRetry) {
throw Exception('Unauthorized after retry');
}
await AuthViewModel.instance.login();
return validateAction(uuid, code, isRetry: true);

case ApiSuccess<http.Response>():
return;

case ApiFailure<http.Response>(:final error):
throw HttpException(error);
}
}

String _extractUuid(final http.Response response) {
return (jsonDecode(utf8.decode(response.bodyBytes))
as Map<String, dynamic>)['uuid']
as String;
}
}
Loading
Loading