Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3cdbeeb
feat(deep_links): add deep linking support for iOS & dynamic url sche…
marfavi Jan 22, 2026
b798ae1
set initial location to login screen
marfavi Jan 23, 2026
fb1fc8f
make basic version of login screen w/o logic
marfavi Jan 23, 2026
eb243ed
add navigation when tapping "log out" in settings screen
marfavi Jan 23, 2026
d2b3edf
avoid overflow on large zoom in login screen & move loading overlay t…
marfavi Jan 23, 2026
b461570
use flexfit.tight instead of center widget
marfavi Jan 23, 2026
ddcb578
depend on flutter_secure_storage package
marfavi Jan 26, 2026
7f1e814
overhaul login/auth
marfavi Jan 26, 2026
1ac9937
add tests
marfavi Jan 26, 2026
0da59a0
change production launch config to target development flavor
marfavi Jan 26, 2026
c21a38b
sort imports
marfavi Jan 26, 2026
e644add
authorize -> authenticate
marfavi Jan 26, 2026
3e1e42e
adjust names and doc comments
marfavi Jan 26, 2026
4aa96a8
a rename
marfavi Jan 26, 2026
81f47bb
feat(assets): add new background images
MonirMooghen Jan 23, 2026
7c24bd8
fix: adjust colour blending options & add half-height version of bean…
marfavi Jan 23, 2026
521b39a
fix: render ink splash above ticket card background
marfavi Jan 23, 2026
10eb070
fix(use_ticket): put content over splash effect on TicketCardBase
marfavi Jan 23, 2026
73f37f6
refactor(http): simplify NetworkRequestExecutor API (#26)
marfavi Feb 9, 2026
870295d
chore: push progress
marfavi Feb 9, 2026
f083487
Merge branch 'om-magic-login' of https://github.com/AnalogIO/mobile_a…
marfavi Feb 9, 2026
9f19ddc
fix analyse errors
marfavi Feb 9, 2026
a3f7fde
Fix tests failing in router_test.dart
marfavi Feb 9, 2026
a861485
add coverage target in Makefile
marfavi Feb 9, 2026
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
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ else
SED_INPLACE := sed -i -E
endif

.PHONY: help upgrade generate clean get swagger
.PHONY: help upgrade generate clean get swagger coverage fix

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -37,3 +37,11 @@ get: ## Get dependencies
swagger: ## Fetch latest Swagger API specs
curl -o swagger/coffeecard_api_v1.json https://core.dev.analogio.dk/swagger/v1/swagger.json
curl -o swagger/coffeecard_api_v2.json https://core.dev.analogio.dk/swagger/v2/swagger.json

coverage: ## Run tests with coverage and generate report
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html

fix: ## Format code and fix issues that can be fixed automatically
dart format .
dart fix --apply
7 changes: 7 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ PODS:
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS

DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)

EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb

PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

Expand Down
23 changes: 13 additions & 10 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,30 @@ import 'package:cafe_analog_app/app/app_brightness_provider.dart';
import 'package:cafe_analog_app/app/dependencies_provider.dart';
import 'package:cafe_analog_app/app/router.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class App extends StatefulWidget {
const App({super.key});
const App({required this.localStorage, super.key});

final SharedPreferencesWithCache localStorage;

@override
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
// TODO(marfavi): Remove theme switching when all widgets support system theme
Brightness _brightness = Brightness.light;

void _setBrightness(Brightness brightness) {
setState(() => _brightness = brightness);
}
Brightness appBrightness = .light;

@override
Widget build(BuildContext context) {
return DependenciesProvider(
localStorage: widget.localStorage,
child: MaterialApp.router(
routerConfig: AnalogGoRouter.instance.goRouter,
theme: ThemeData(
brightness: _brightness,
colorScheme: ColorScheme.fromSeed(
brightness: _brightness,
brightness: appBrightness,
colorScheme: .fromSeed(
brightness: appBrightness,
seedColor: const Color(0xFF785B38),
),
),
Expand All @@ -39,4 +38,8 @@ class _AppState extends State<App> {
),
);
}

void _setBrightness(Brightness newBrightness) {
setState(() => appBrightness = newBrightness);
}
}
35 changes: 0 additions & 35 deletions lib/app/bootstrap.dart

This file was deleted.

56 changes: 48 additions & 8 deletions lib/app/dependencies_provider.dart
Original file line number Diff line number Diff line change
@@ -1,39 +1,79 @@
import 'dart:async';

import 'package:cafe_analog_app/core/http_client.dart';
import 'package:cafe_analog_app/core/network_request_executor.dart';
import 'package:cafe_analog_app/core/network_request_interceptor.dart';
import 'package:cafe_analog_app/generated/api/coffeecard_api_v1.swagger.dart'
hide $JsonSerializableConverter;
import 'package:cafe_analog_app/generated/api/coffeecard_api_v2.swagger.dart';
import 'package:cafe_analog_app/login/bloc/authentication_cubit.dart';
import 'package:cafe_analog_app/login/data/authentication_token_repository.dart';
import 'package:cafe_analog_app/login/data/login_repository.dart';
import 'package:chopper/chopper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// Provides the dependencies required throughout the app.
class DependenciesProvider extends StatelessWidget {
const DependenciesProvider({required this.child, super.key});
const DependenciesProvider({
required this.localStorage,
required this.child,
super.key,
});

final SharedPreferencesWithCache localStorage;
final MaterialApp child;

@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
// Persistence
RepositoryProvider.value(value: localStorage),
RepositoryProvider(create: (_) => const FlutterSecureStorage()),
RepositoryProvider.value(value: apiV1),
RepositoryProvider.value(value: apiV2),
RepositoryProvider(create: (_) => Logger()),
RepositoryProvider(create: (_) => AuthTokenStore()),

// Http
RepositoryProvider(
create: (context) => NetworkRequestExecutor(logger: context.read()),
create: (context) => ChopperClient(
baseUrl: Uri.parse('https://core.dev.analogio.dk'),
interceptors: [
NetworkRequestInterceptor(authTokenStore: context.read()),
],
converter: $JsonSerializableConverter(),
services: [CoffeecardApiV1.create(), CoffeecardApiV2.create()],
// FIXME(marfavi): Add authenticator to redirect on 401 responses
// authenticator: sl.get<ReactivationAuthenticator>(),
),
),
RepositoryProvider(
create: (context) =>
LoginRepository(apiV2: context.read(), executor: context.read()),
context.read<ChopperClient>().getService<CoffeecardApiV1>(),
),
RepositoryProvider(
create: (context) =>
AuthTokenRepository(secureStorage: context.read()),
context.read<ChopperClient>().getService<CoffeecardApiV2>(),
),
RepositoryProvider(create: (_) => Logger()),
RepositoryProvider(
create: (context) => NetworkRequestExecutor(
logger: context.read(),
apiV1: context.read(),
apiV2: context.read(),
),
),

// Auth/login repositories
RepositoryProvider(
create: (context) => LoginRepository(executor: context.read()),
),
RepositoryProvider(
create: (context) => AuthTokenRepository(
secureStorage: context.read(),
authTokenStore: context.read(),
),
),
],
child: MultiBlocProvider(
Expand Down
2 changes: 1 addition & 1 deletion lib/app/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'package:cafe_analog_app/stats/view/stats_screen.dart';
import 'package:cafe_analog_app/tickets/buy_tickets/buy_tickets_screen.dart';
import 'package:cafe_analog_app/tickets/buy_tickets/product.dart';
import 'package:cafe_analog_app/tickets/buy_tickets/ticket_detail_screen.dart';
import 'package:cafe_analog_app/tickets/my_tickets/tickets_screen.dart';
import 'package:cafe_analog_app/tickets/my_tickets/ui/tickets_screen.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/app/splash_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:cafe_analog_app/core/widgets/analog_circular_progress_indicator.dart';
import 'package:cafe_analog_app/tickets/use_ticket/delayed_fade_in.dart';
import 'package:cafe_analog_app/core/widgets/delayed_fade_in.dart';
import 'package:flutter/material.dart';

class SplashScreen extends StatelessWidget {
Expand Down
15 changes: 0 additions & 15 deletions lib/core/http_client.dart

This file was deleted.

2 changes: 1 addition & 1 deletion lib/core/loading_overlay.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:async';

import 'package:cafe_analog_app/core/widgets/analog_circular_progress_indicator.dart';
import 'package:cafe_analog_app/tickets/use_ticket/delayed_fade_in.dart';
import 'package:cafe_analog_app/core/widgets/delayed_fade_in.dart';
import 'package:flutter/material.dart';

/// Shows a loading overlay dialog.
Expand Down
34 changes: 30 additions & 4 deletions lib/core/network_request_executor.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import 'package:cafe_analog_app/core/failures.dart';
import 'package:cafe_analog_app/generated/api/client_index.dart';
import 'package:chopper/chopper.dart' show Response;
import 'package:fpdart/fpdart.dart';
import 'package:logger/logger.dart';

class NetworkRequestExecutor {
const NetworkRequestExecutor({required this.logger});
const NetworkRequestExecutor({
required this.logger,
required this.apiV1,
required this.apiV2,
});

final Logger logger;
final CoffeecardApiV1 apiV1;
final CoffeecardApiV2 apiV2;

/// Executes a network request and returns a [TaskEither].
///
/// If the request fails, a [NetworkFailure] is returned in a [Left].
/// If the request fails, a [Failure] is returned in a [Left].
/// If the request succeeds, the response body of type
/// [BodyType] is returned in a [Right].
TaskEither<NetworkFailure, BodyType> execute<BodyType>(
///
/// ```dart
/// executor.run((api) => api.v2.accountAuthPost(...));
/// ```
TaskEither<Failure, BodyType> run<BodyType>(
Future<Response<BodyType>> Function(ApiClients api) request,
) {
final clients = ApiClients(v1: apiV1, v2: apiV2);
return _execute<BodyType>(() => request(clients));
}

TaskEither<Failure, BodyType> _execute<BodyType>(
Future<Response<BodyType>> Function() request,
) {
return TaskEither<NetworkFailure, Response<BodyType>>.tryCatch(
return TaskEither<Failure, Response<BodyType>>.tryCatch(
request,
(error, stackTrace) {
logger.e(error.toString());
Expand All @@ -34,3 +52,11 @@ class NetworkRequestExecutor {
);
}
}

/// Small helper that groups generated API clients.
class ApiClients {
const ApiClients({required this.v1, required this.v2});

final CoffeecardApiV1 v1;
final CoffeecardApiV2 v2;
}
29 changes: 29 additions & 0 deletions lib/core/network_request_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:async';

import 'package:chopper/chopper.dart';

class AuthTokenStore {
String? token;
}

class NetworkRequestInterceptor implements Interceptor {
NetworkRequestInterceptor({
required AuthTokenStore authTokenStore,
}) : _authTokenStore = authTokenStore;

final AuthTokenStore _authTokenStore;

@override
FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) {
return chain.proceed(
switch (_authTokenStore.token) {
null => chain.request,
final token => applyHeader(
chain.request,
'Authorization',
'Bearer $token',
),
},
);
}
}
1 change: 1 addition & 0 deletions lib/core/widgets/app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class AnalogAppBar extends StatelessWidget implements PreferredSizeWidget {
this.onBrightnessChanged,
super.key,
});

final String title;
final void Function(Brightness)? onBrightnessChanged;

Expand Down
39 changes: 39 additions & 0 deletions lib/core/widgets/delayed_fade_in.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';

/// A widget that fades in its child after a specified delay.
class DelayedFadeIn extends StatefulWidget {
const DelayedFadeIn({
required this.child,
this.delay = const Duration(milliseconds: 250),
super.key,
});

final Widget child;
final Duration delay;

@override
State<DelayedFadeIn> createState() => _DelayedFadeInState();
}

class _DelayedFadeInState extends State<DelayedFadeIn> {
bool _visible = false;

@override
void initState() {
super.initState();
Future.delayed(widget.delay, () {
if (mounted) {
setState(() => _visible = true);
}
});
}

@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
child: widget.child,
);
}
}
Loading