Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
618447a
Adds tests for change_email_verification.dart
JasonYoder2026 Mar 20, 2026
867a7e0
Separates edit_profile.dart
JasonYoder2026 Mar 20, 2026
3787749
Correct edit_profile feature
JasonYoder2026 Mar 20, 2026
bd49a11
Adds tests for edit_profile feature
JasonYoder2026 Mar 20, 2026
c9e9f17
Cleans tests
JasonYoder2026 Mar 20, 2026
141e742
Separates email_verification.dart
JasonYoder2026 Mar 20, 2026
6cdabb6
Adds tests for email_verification feature
JasonYoder2026 Mar 20, 2026
a9a7b71
Separates home page feature
JasonYoder2026 Mar 20, 2026
0a0a38e
Adds tests for home feature
JasonYoder2026 Mar 20, 2026
02c2560
Separates loading feature
JasonYoder2026 Mar 20, 2026
9eaa0b4
Fix controller parameters
JasonYoder2026 Mar 20, 2026
f36e95e
Refactor loading
JasonYoder2026 Mar 20, 2026
a39683d
Adds tests for loading
JasonYoder2026 Mar 20, 2026
3ebcd9e
Cleans tests
JasonYoder2026 Mar 20, 2026
47ce4d8
Separates login into features/login
JasonYoder2026 Mar 20, 2026
9ba130f
Refactors controller to use DI parameters
JasonYoder2026 Mar 20, 2026
682f001
Adds tests for login
JasonYoder2026 Mar 20, 2026
656572d
Separates loyalty_card_page.dart into loyalty feature
JasonYoder2026 Mar 21, 2026
ff9d6f5
Separates loyalty_card_page.dart into loyalty feature
JasonYoder2026 Mar 21, 2026
9e00dca
Removes unneeded DI
JasonYoder2026 Mar 21, 2026
1c40599
Adds tests for loyalty feature
JasonYoder2026 Mar 21, 2026
4aabc22
Separates monthly report
JasonYoder2026 Mar 21, 2026
6bdda7c
Adds tests for monthly report
JasonYoder2026 Mar 21, 2026
3cf788e
Moves not found page to features
JasonYoder2026 Mar 21, 2026
12aee0c
Separates reset password
JasonYoder2026 Mar 21, 2026
4cb3ca1
Fixes controller by adding trimming
JasonYoder2026 Mar 21, 2026
4881cef
Adds tests for password_reset
JasonYoder2026 Mar 21, 2026
6732781
Separates payment page into feature
JasonYoder2026 Mar 21, 2026
a1920cd
Renames payment_page to machine_payment
JasonYoder2026 Mar 21, 2026
94c6909
Adds tests for machine_payment
JasonYoder2026 Mar 21, 2026
f1684e9
Separates request refund
JasonYoder2026 Mar 22, 2026
b95d258
Adds tests for refund
JasonYoder2026 Mar 22, 2026
efa18fe
Separates reset_protected
JasonYoder2026 Mar 22, 2026
9fe4fdb
Adds tests for reset_protected
JasonYoder2026 Mar 22, 2026
79e395b
Moves root_app to lib/
JasonYoder2026 Mar 22, 2026
b7850ac
Separates scanner
JasonYoder2026 Mar 22, 2026
002a6a4
Adds tests for scanner
JasonYoder2026 Mar 22, 2026
90e2b76
Separates settings
JasonYoder2026 Mar 22, 2026
2dfe5d6
Adds tests for settings
JasonYoder2026 Mar 23, 2026
fde8cab
Separates signup
JasonYoder2026 Mar 23, 2026
e06a678
Adds tests for sign_up
JasonYoder2026 Mar 23, 2026
e9b11fc
Separates start_machine
JasonYoder2026 Mar 23, 2026
07d2f66
Adds tests for start_machine
JasonYoder2026 Mar 23, 2026
4aeb438
Separates verification code
JasonYoder2026 Mar 23, 2026
c73ace2
Adds tests for code verification
JasonYoder2026 Mar 23, 2026
d00de02
Fixes routes in tests
JasonYoder2026 Mar 23, 2026
85896c6
Replaces pages in router with features
JasonYoder2026 Mar 23, 2026
8ef0bce
Fixes missing colon
JasonYoder2026 Mar 23, 2026
7682ef1
Fixes qr scanner
JasonYoder2026 Mar 23, 2026
0d0a7bd
Removes pages/ replaces pages with features
JasonYoder2026 Mar 23, 2026
5e111f4
Moves widget
JasonYoder2026 Mar 23, 2026
2f5f087
Cleans up entry point
JasonYoder2026 Mar 23, 2026
bbb8f26
Fixes DI in tests
JasonYoder2026 Mar 23, 2026
4820534
Moves router to core/
JasonYoder2026 Mar 23, 2026
1fb461f
Moves theme to core/
JasonYoder2026 Mar 23, 2026
02a77a9
Adjusts imports and tests
JasonYoder2026 Mar 23, 2026
2e7ffa9
Adjusts imports
JasonYoder2026 Mar 23, 2026
c70a1aa
Merge pull request #114 from jamaki604/architecture
JasonYoder2026 Mar 24, 2026
578f03a
Merge branch 'main' into dev
JasonYoder2026 Mar 24, 2026
e50bc3e
Added Maintenance Request Page
JMiller928172 Apr 6, 2026
d80093f
Added Photo Field
JMiller928172 Apr 6, 2026
f6e1b72
Added maintenance categories
JMiller928172 Apr 6, 2026
07612d1
Removed maintenance disclaimer
JMiller928172 Apr 6, 2026
bf4454b
Aquired Permissions
JMiller928172 Apr 7, 2026
09669ff
Added tests
JMiller928172 Apr 13, 2026
984866a
Added tests for maintenance_form
JMiller928172 Apr 13, 2026
4a68741
Added header tests
JMiller928172 Apr 13, 2026
1695e9f
Added tests for maintenance card
JMiller928172 Apr 13, 2026
9287227
Fixed maintenance_request_test
JMiller928172 Apr 13, 2026
1576e01
Removed enter key submission
JMiller928172 Apr 13, 2026
2f7439e
Adjusted for supabase upload
JMiller928172 Apr 15, 2026
71d46e2
Added locations to maint_requests
JMiller928172 Apr 15, 2026
53e4978
Reduced space
JMiller928172 Apr 15, 2026
50c53fb
Reduced logo height
JMiller928172 Apr 15, 2026
b55d0c3
Added Location Maint Category
JMiller928172 Apr 15, 2026
864527c
changed email verification to using code instead of email link
karelinejones Apr 15, 2026
526e781
Fixes denyRefund tests
JasonYoder2026 Apr 18, 2026
07f36d0
Fixes approveRefund tests
JasonYoder2026 Apr 18, 2026
5056e37
Merge pull request #115 from jamaki604/request-maintenance
JasonYoder2026 Apr 20, 2026
48f7764
fixed failed tests
karelinejones Apr 20, 2026
70a1509
Fixed credit-card spanning
JMiller928172 Apr 21, 2026
6e3a59d
Fixed home page spanning issues
JMiller928172 Apr 21, 2026
0b9454c
Merge pull request #117 from jamaki604/backend-tests
JasonYoder2026 Apr 21, 2026
6ee150d
Merge pull request #118 from jamaki604/spanning-fix
JMiller928172 Apr 21, 2026
4178417
Merge pull request #116 from jamaki604/email-verification-fix
JMiller928172 Apr 21, 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
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
Expand Down
101 changes: 101 additions & 0 deletions lib/core/di/di.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:go_router/go_router.dart';

// services
import 'package:clean_stream_laundry_app/logic/services/auth_service.dart';
import 'package:clean_stream_laundry_app/logic/services/edge_function_service.dart';
import 'package:clean_stream_laundry_app/logic/services/location_service.dart';
import 'package:clean_stream_laundry_app/logic/services/machine_communication_service.dart';
import 'package:clean_stream_laundry_app/logic/services/machine_service.dart';
import 'package:clean_stream_laundry_app/logic/services/payment_service.dart';
import 'package:clean_stream_laundry_app/logic/services/profile_service.dart';
import 'package:clean_stream_laundry_app/logic/services/transaction_service.dart';

// implementations
import 'package:clean_stream_laundry_app/services/supabase/supabase_auth_service.dart';
import 'package:clean_stream_laundry_app/services/supabase/supabase_edge_function_service.dart';
import 'package:clean_stream_laundry_app/services/supabase/supabase_location_service.dart';
import 'package:clean_stream_laundry_app/services/supabase/supabase_machine_service.dart';
import 'package:clean_stream_laundry_app/services/supabase/supabase_profile_service.dart';
import 'package:clean_stream_laundry_app/services/supabase/supabase_transaction_service.dart';
import 'package:clean_stream_laundry_app/services/stripe/stripe_service.dart';
import 'package:clean_stream_laundry_app/services/nayax/machine_communicator.dart';

// misc
import 'package:clean_stream_laundry_app/core/router/app_router.dart';
import 'package:clean_stream_laundry_app/services/notification_service.dart';
import 'package:clean_stream_laundry_app/logic/payment/process_payment.dart';

final getIt = GetIt.instance;

Future<void> setupDependencies() async {
await dotenv.load(fileName: '.env');

await Supabase.initialize(
url: dotenv.env['SUPABASE_URL']!,
anonKey: dotenv.env['ANON_KEY']!,
);

final supabase = Supabase.instance.client;

Stripe.publishableKey = dotenv.env['STRIPE_PUBLISHABLE_KEY']!;

getIt.registerLazySingleton<TransactionService>(
() => SupabaseTransactionService(client: supabase),
);

getIt.registerLazySingleton<ProfileService>(
() => SupabaseProfileService(client: supabase),
);

getIt.registerLazySingleton<MachineService>(
() => SupabaseMachineService(client: supabase),
);

getIt.registerLazySingleton<LocationService>(
() => SupabaseLocationHandler(client: supabase),
);

getIt.registerLazySingleton<EdgeFunctionService>(
() => SupabaseEdgeFunctionService(client: supabase),
);

getIt.registerLazySingleton<AuthService>(
() => SupabaseAuthService(client: supabase),
);

getIt.registerLazySingleton<PaymentService>(() => StripeService());

getIt.registerLazySingleton<Stripe>(() => Stripe.instance);

getIt.registerLazySingleton<MachineCommunicationService>(
() => MachineCommunicator(
edgeFunctionService: getIt<EdgeFunctionService>(),
),
);

getIt.registerLazySingleton<RouterService>(() => RouterService());

getIt.registerLazySingleton<NotificationService>(
() => NotificationService(),
);

getIt.registerSingleton<FlutterLocalNotificationsPlugin>(
FlutterLocalNotificationsPlugin(),
);

getIt.registerLazySingleton<PaymentProcessor>(
() => PaymentProcessor(),
);

getIt.registerLazySingleton<GoRouter>(() {
final authService = getIt<AuthService>();
final routerService = getIt<RouterService>();

return routerService.createRouter(authService);
});
}
80 changes: 47 additions & 33 deletions lib/middleware/app_router.dart → lib/core/router/app_router.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import 'package:app_links/app_links.dart';
import 'package:clean_stream_laundry_app/pages/change_email_verification.dart';
import 'package:clean_stream_laundry_app/pages/edit_profile_page.dart';
import 'package:clean_stream_laundry_app/pages/verify_code_page.dart';
import 'package:go_router/go_router.dart';
import 'package:clean_stream_laundry_app/features/change_email_verification/change_email_verification.dart';
import 'package:clean_stream_laundry_app/features/edit_profile/edit_profile.dart';
import 'package:clean_stream_laundry_app/features/verify_code/verify_code.dart';
import 'package:clean_stream_laundry_app/features/email_verification/email_verification.dart';
import 'package:clean_stream_laundry_app/features/home/home.dart';
import 'package:clean_stream_laundry_app/features/loading/loading.dart';
import 'package:clean_stream_laundry_app/features/loyalty/loyalty.dart';
import 'package:clean_stream_laundry_app/features/scanner/scanner.dart';
import 'package:clean_stream_laundry_app/features/sign_up/sign_up.dart';
import 'package:clean_stream_laundry_app/features/login/login.dart';
import 'package:clean_stream_laundry_app/features/not_found/not_found.dart';
import 'package:clean_stream_laundry_app/features/settings/settings.dart';
import 'package:clean_stream_laundry_app/features/start_machine/start_machine.dart';
import 'package:clean_stream_laundry_app/features/machine_payment/machine_payment.dart';
import 'package:clean_stream_laundry_app/features/monthly_report/monthly_report.dart';
import 'package:clean_stream_laundry_app/features/refund_request/refund_request.dart';
import 'package:clean_stream_laundry_app/features/maintenance_request/maintenance_request.dart';
import 'package:clean_stream_laundry_app/features/password_reset/password_reset.dart';
import 'package:clean_stream_laundry_app/features/reset_protected/reset_protected.dart';
import 'package:clean_stream_laundry_app/logic/services/auth_service.dart';
import 'package:clean_stream_laundry_app/pages/email_verification_page.dart';
import 'package:clean_stream_laundry_app/pages/home_page.dart';
import 'package:clean_stream_laundry_app/pages/loading_page.dart';
import 'package:clean_stream_laundry_app/pages/loyalty_card_page.dart';
import 'package:clean_stream_laundry_app/pages/scanner_widget.dart';
import 'package:clean_stream_laundry_app/pages/sign_up_screen.dart';
import 'package:clean_stream_laundry_app/pages/login_page.dart';
import 'package:clean_stream_laundry_app/pages/not_found_page.dart';
import 'package:clean_stream_laundry_app/pages/settings.dart';
import 'package:clean_stream_laundry_app/pages/start_machine_page.dart';
import 'package:clean_stream_laundry_app/pages/payment_page.dart';
import 'package:clean_stream_laundry_app/pages/monthly_transaction_history.dart';
import 'package:clean_stream_laundry_app/pages/refund_page.dart';
import 'package:clean_stream_laundry_app/pages/password_reset.dart';
import 'package:clean_stream_laundry_app/pages/reset_protected_page.dart';
import 'package:go_router/go_router.dart';

class RouterService {
GoRouter createRouter(AuthService authenticator) => GoRouter(
Expand All @@ -28,7 +29,7 @@ class RouterService {
path: '/login',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: LoginScreen(appLinks: AppLinks()),
child: Login(appLinks: AppLinks()),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
Expand All @@ -38,7 +39,7 @@ class RouterService {
path: '/signup',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: SignUpScreen(),
child: SignUpPage(),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
Expand All @@ -48,7 +49,7 @@ class RouterService {
path: '/scanner',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: ScannerWidget(),
child: ScannerPage(),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
Expand Down Expand Up @@ -81,7 +82,7 @@ class RouterService {
final machineId = state.uri.queryParameters['machineId'] ?? '';
return CustomTransitionPage(
key: state.pageKey,
child: PaymentPage(machineId: machineId),
child: MachinePayment(machineId: machineId),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
Expand All @@ -90,13 +91,16 @@ class RouterService {
),
GoRoute(
path: '/email-verification',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: EmailVerificationPage(appLinks: AppLinks()),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
),
pageBuilder: (context, state) {
final email = state.extra is String ? state.extra as String : null;
return CustomTransitionPage(
key: state.pageKey,
child: EmailVerificationPage(email: email),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
);
},
),
GoRoute(
path: '/homePage',
Expand Down Expand Up @@ -134,7 +138,7 @@ class RouterService {
final transactions = state.extra as List<Map<String, dynamic>>? ?? [];
return CustomTransitionPage(
key: state.pageKey,
child: MonthlyTransactionHistory(transactions: transactions),
child: MonthlyReport(transactions: transactions),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
Expand All @@ -151,6 +155,16 @@ class RouterService {
transitionsBuilder: (_, _, _, child) => child,
),
),
GoRoute(
path: '/maintenancePage',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: MaintenancePage(),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionsBuilder: (_, _, _, child) => child,
),
),
GoRoute(
path: '/editProfile',
pageBuilder: (context, state) => CustomTransitionPage(
Expand Down Expand Up @@ -195,14 +209,14 @@ class RouterService {
transitionsBuilder: (_, _, _, child) => child,
);
},
)
),
],
errorBuilder: (context, state) {
final uri = state.uri;
if (uri.scheme == 'clean-stream' && uri.host == 'reset-protected') {
return ResetProtectedPage();
}
return const NotFoundScreen();
return const NotFound();
},
redirect: (context, state) {
final uri = state.uri;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:clean_stream_laundry_app/logic/theme/theme.dart';
import 'package:clean_stream_laundry_app/middleware/storage_service.dart';
import 'package:clean_stream_laundry_app/core/theme/theme.dart';
import 'package:clean_stream_laundry_app/core/storage/storage_service.dart';

class ThemeManager with ChangeNotifier {
ThemeData _themeData = lightMode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:clean_stream_laundry_app/Logic/Theme/theme.dart';
import 'package:clean_stream_laundry_app/core/theme/theme.dart';
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'controller.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:clean_stream_laundry_app/Logic/Theme/theme.dart';
import 'package:clean_stream_laundry_app/core/theme/theme.dart';
import 'package:flutter/material.dart';

class VerificationError extends StatelessWidget {
Expand Down
113 changes: 113 additions & 0 deletions lib/features/edit_profile/controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:clean_stream_laundry_app/logic/services/auth_service.dart';
import 'package:clean_stream_laundry_app/logic/services/profile_service.dart';
import 'package:clean_stream_laundry_app/logic/services/edge_function_service.dart';

class EditProfileController extends ChangeNotifier {
final authService = GetIt.instance<AuthService>();
final profileService = GetIt.instance<ProfileService>();
final edgeFunctionService = GetIt.instance<EdgeFunctionService>();

final nameController = TextEditingController();
final emailController = TextEditingController();

StreamSubscription? authSub;

String currentName = '';
String currentEmail = '';

bool isLoading = true;
bool isSaving = false;

bool get hasChanges =>
nameController.text.trim() != currentName ||
emailController.text.trim() != currentEmail;

Future<void> init() async {
await loadUserData();

authSub = authService.onAuthChange.listen((_) {
loadUserData();
});
}

Future<void> disposeController() async {
nameController.dispose();
emailController.dispose();
await authSub?.cancel();
}

Future<void> loadUserData() async {
try {
final userId = await authService.getCurrentUserId;

if (userId == null) {
throw Exception("User not found");
}

final username = await profileService.getUserNameById(userId);
final email = await authService.getCurrentUserEmail();

currentName = username ?? '';
currentEmail = email ?? '';

nameController.text = currentName;
emailController.text = currentEmail;
} catch (e) {
rethrow;
} finally {
isLoading = false;
notifyListeners();
}
}

Future<bool> saveChanges() async {
if (isSaving) return false;

if (!hasChanges) {
throw Exception("No changes made");
}

final newName = nameController.text.trim();
final newEmail = emailController.text.trim();

final nameChanged = newName != currentName;
final emailChanged = newEmail != currentEmail;

isSaving = true;
notifyListeners();

try {
await authService.updateUserAttributes(
email: emailChanged ? newEmail : null,
data: nameChanged ? {'full_name': newName} : null,
);

currentName = newName;
currentEmail = newEmail;

return emailChanged;
} finally {
isSaving = false;
notifyListeners();
}
}

Future<bool> deleteAccount() async {
final userId = await authService.getCurrentUserId;

final response = await edgeFunctionService.runEdgeFunction(
name: "delete-account",
body: {"user_id": userId},
);

if (response?.status == 200) {
await authService.logout();
return true;
}

return false;
}
}
Loading
Loading