diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index da426b3..7f9cdbe 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -46,7 +46,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
- android:launchMode="singleTop"
+ android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
+
diff --git a/lib/main.dart b/lib/main.dart
index 0165014..9842225 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -12,6 +12,7 @@ import 'package:mc_common_app/services/firebase_service.dart';
import 'package:mc_common_app/utils/location/Location.dart';
import 'package:mc_common_app/view_models/dashboard_view_model_provider.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
+import 'package:mc_common_app/views/splash/splash_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
@@ -135,7 +136,7 @@ Future main() async {
paymentService: injector.get(),
paymentRepo: injector.get(),
),
- ),
+ ),
ChangeNotifierProvider(
create: (_) => ShippingManagementVM(
shippingRepo: injector.get(),
@@ -194,6 +195,42 @@ class _MyAppState extends State {
locale: context.locale,
initialRoute: AppRoutes.initialRoute,
routes: ProviderAppRoutes.routes,
+ navigatorObservers: [AdDeepLinkService.routeObserver],
+ // Confirmed on a real device, two different unknown-route cases land here:
+ // 1) Cold-start restoration can try to regenerate a bare "/" instead of
+ // respecting initialRoute -- uncaught, that's a "Could not find a generator
+ // for route" crash before AdDeepLinkService ever runs. Splash is a real,
+ // correct landing page for this one.
+ // 2) On an ALREADY-RUNNING app, Android's Flutter embedding auto-forwards the
+ // incoming deep-link intent's path as its own route push (e.g. "/1541" from
+ // mowaterprovider://ad/1541) independently of app_links/AdDeepLinkService,
+ // which already handles that same uri correctly. Since "/1541" isn't a real
+ // route, it used to fall into case 1 and push Splash right over whatever
+ // screen (e.g. the dashboard) was already showing -- looking exactly like the
+ // app had restarted. Anything other than bare "/" must be a no-op here: push
+ // something inert that removes itself immediately instead of a real page.
+ onUnknownRoute: (settings) {
+ if (settings.name == '/') {
+ return MaterialPageRoute(builder: (_) => const SplashPage());
+ }
+ // Remove THIS specific route once inserted, not just "whatever is on top" --
+ // AdDeepLinkService's own (legitimate) push can land after this one, and a
+ // plain Navigator.pop() would close that instead of this inert placeholder.
+ late final PageRoute route;
+ route = PageRouteBuilder(
+ settings: const RouteSettings(name: AdDeepLinkService.ignoredPlaceholderRouteName),
+ opaque: false,
+ transitionDuration: Duration.zero,
+ reverseTransitionDuration: Duration.zero,
+ pageBuilder: (context, __, ___) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (route.isActive) route.navigator?.removeRoute(route);
+ });
+ return const SizedBox.shrink();
+ },
+ );
+ return route;
+ },
),
);
},
diff --git a/lib/views/subscriptions/all_subscriptions_page.dart b/lib/views/subscriptions/all_subscriptions_page.dart
index 359bdb9..f4185bf 100644
--- a/lib/views/subscriptions/all_subscriptions_page.dart
+++ b/lib/views/subscriptions/all_subscriptions_page.dart
@@ -84,7 +84,7 @@ class _SubscriptionsPageState extends State {
showMyBottomSheet(context, child: UpgradePackageSheet(subscription: subcriptionsVM.tempSubscriptions[index], isRenew: false));
},
onDowngradeSubscriptionClick: () {
- showMyBottomSheet(context, child: DowngradeDataSheet(subcriptionsVM.tempSubscriptions[index]));
+ showMyBottomSheet(context, child: DowngradeDataSheet(subcriptionsVM.tempSubscriptions[index], ));
},
);
},
diff --git a/lib/views/subscriptions/sheet/downgrade_data_sheet.dart b/lib/views/subscriptions/sheet/downgrade_data_sheet.dart
index c9bb1c3..353297a 100644
--- a/lib/views/subscriptions/sheet/downgrade_data_sheet.dart
+++ b/lib/views/subscriptions/sheet/downgrade_data_sheet.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
+import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/general_models/generic_resp_model.dart';
import 'package:mc_common_app/models/subscriptions_models/branch_user_selection_model.dart';
import 'package:mc_common_app/models/subscriptions_models/subscription_model.dart';
@@ -15,6 +16,7 @@ import 'package:mc_common_app/views/payments/payment_methods_view.dart';
import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:provider/provider.dart';
+import 'package:easy_localization/easy_localization.dart';
class DowngradeDataSheet extends StatefulWidget {
final Subscription subscription;
@@ -32,8 +34,65 @@ class _DowngradeDataSheetState extends State {
context.read().getSPBranchUserGet();
}
+ // The provider's current active plan (not the plan being downgraded to) holds how many
+ // branches/sub-users they currently have -- confirmed with the user this is where
+ // "current totals" come from, as opposed to widget.subscription which is the target plan.
+ Subscription? get _currentSubscription {
+ final list = AppState().getproviderSubscription;
+ return list.isEmpty ? null : list.first;
+ }
+
+ // subscriptionBranches/subscriptionSubUsers are each plan's own allowed quota.
+ // totalBranches/totalSubUsers looked plausible but turned out to be unrelated (they
+ // read identically on both the current and target plan records in testing).
+ int get _currentBranches => _currentSubscription?.subscriptionBranches ?? 0;
+
+ int get _currentUsers => _currentSubscription?.subscriptionSubUsers ?? 0;
+
+ int get _targetBranches => widget.subscription.subscriptionBranches ?? 0;
+
+ int get _targetUsers => widget.subscription.subscriptionSubUsers ?? 0;
+
+ int get _branchesToRemove => (_currentBranches - _targetBranches).clamp(0, _currentBranches);
+
+ int get _usersToRemove => (_currentUsers - _targetUsers).clamp(0, _currentUsers);
+
+ // Selecting a whole branch removes every user in it too, so that counts towards the
+ // user quota as well, not just the branch quota.
+ _SelectedCounts _selectedCounts(List branchSelectionList) {
+ int selectedBranches = 0;
+ int selectedUsers = 0;
+ for (var branch in branchSelectionList) {
+ if (branch.isSelected) {
+ selectedBranches++;
+ selectedUsers += branch.usersList.length;
+ } else {
+ selectedUsers += branch.usersList.where((u) => u.isSelected).length;
+ }
+ }
+ return _SelectedCounts(selectedBranches, selectedUsers);
+ }
+
+ Widget _buildRequirementCounter(List branchSelectionList) {
+ final counts = _selectedCounts(branchSelectionList);
+ final remainingBranches = (_branchesToRemove - counts.branches).clamp(0, _branchesToRemove);
+ final remainingUsers = (_usersToRemove - counts.users).clamp(0, _usersToRemove);
+
+ if (remainingBranches == 0 && remainingUsers == 0) {
+ return "You have selected enough branches/users to downgrade.".toText(fontSize: 13, color: MyColors.primaryColor, isBold: true);
+ }
+
+ final parts = [
+ if (remainingBranches > 0) "$remainingBranches more branch${remainingBranches == 1 ? '' : 'es'}",
+ if (remainingUsers > 0) "$remainingUsers more user${remainingUsers == 1 ? '' : 's'}",
+ ];
+ return "Select ${parts.join(' and ')} to downgrade.".toText(fontSize: 13, color: MyColors.textColor, isBold: true);
+ }
+
@override
Widget build(BuildContext context) {
+ final targetBranchesText = "$_targetBranches branch${_targetBranches == 1 ? '' : 'es'}";
+ final targetUsersText = "$_targetUsers sub-user${_targetUsers == 1 ? '' : 's'}";
return SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height / 1.2,
@@ -49,19 +108,44 @@ class _DowngradeDataSheetState extends State {
isBold: true,
),
8.height,
- "To select this subscription you need to deactivate your branches and users as this package only supports 1 branch and 2 users per branch. You can reactivate these branches later after upgrading the package"
- .toText(
+ LocaleKeys.downgradeDeactivateInstruction.tr(namedArgs: {'branches': targetBranchesText, 'users': targetUsersText}).toText(
fontSize: 14,
color: MyColors.textColor,
),
+ 12.height,
+ // isStartNow is passed as false for a downgrade, so the new package isn't
+ // applied immediately -- it takes effect once the current one expires. Make
+ // that explicit so it isn't mistaken for an immediate switch.
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Icon(Icons.info_outline_rounded, size: 18, color: MyColors.textColor),
+ 8.width,
+ Expanded(
+ child: LocaleKeys.downgradeAppliedAfterExpiry.tr().toText(fontSize: 13, color: MyColors.textColor),
+ ),
+ ],
+ ).toContainer(
+ backgroundColor: MyColors.greyButtonColor,
+ paddingAll: 10,
+ borderRadius: 6,
+ ),
],
),
),
Expanded(
child: Consumer(
builder: (BuildContext context, model, Widget? child) {
+ final counts = model.branchSelectionList == null ? _SelectedCounts(0, 0) : _selectedCounts(model.branchSelectionList!);
+ final canProceed = model.branchSelectionList != null && counts.branches >= _branchesToRemove && counts.users >= _usersToRemove;
return Column(
children: [
+ if (model.branchSelectionList != null)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: _buildRequirementCounter(model.branchSelectionList!),
+ ),
+ if (model.branchSelectionList != null) 8.height,
Expanded(
child: model.branchSelectionList == null
? const Center(child: CircularProgressIndicator())
@@ -77,19 +161,23 @@ class _DowngradeDataSheetState extends State {
),
),
ShowFillButton(
- title: "Next",
+ title: LocaleKeys.next.tr(),
maxWidth: double.infinity,
margin: const EdgeInsets.all(12),
+ isDisabled: !canProceed,
onPressed: () async {
List listOfBranches = [];
List listOfUsers = [];
- for (var element in model.branchSelectionList!) {
- if (element.isSelected) {
- listOfBranches.add(element.branchId);
+ for (var branch in model.branchSelectionList!) {
+ if (branch.isSelected) {
+ listOfBranches.add(branch.branchId);
+ for (var user in branch.usersList) {
+ listOfUsers.add(user.userId);
+ }
} else {
- for (var element in element.usersList) {
- if (element.isSelected) {
- listOfUsers.add(element.userId);
+ for (var user in branch.usersList) {
+ if (user.isSelected) {
+ listOfUsers.add(user.userId);
}
}
}
@@ -103,7 +191,7 @@ class _DowngradeDataSheetState extends State {
Utils.showLoading(context);
GenericRespModel response = await model.payForSubscription(
subscriptionId: widget.subscription.id!,
- isStartNow: true,
+ isStartNow: false,
isRenew: false,
amount: widget.subscription.price.toString(),
isDegrade: true,
@@ -152,15 +240,21 @@ class _DowngradeDataSheetState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
- customCheckBox(model.isSelected).onPress(() {
+ // Padding around the checkbox widens its tap target (18x18 alone is too small to
+ // hit reliably) without growing the checkbox's own visual size or changing the
+ // row's separate tap-to-expand behavior.
+ Padding(
+ padding: const EdgeInsets.all(10),
+ child: customCheckBox(model.isSelected),
+ ).onPress(() {
model.isSelected = !model.isSelected;
if (model.isSelected) {
- for (var element in model.usersList) {
- element.isSelected = true;
+ for (var user in model.usersList) {
+ user.isSelected = true;
}
} else {
- for (var element in model.usersList) {
- element.isSelected = false;
+ for (var user in model.usersList) {
+ user.isSelected = false;
}
}
model.isOpend = true;
@@ -265,3 +359,10 @@ class _DowngradeDataSheetState extends State {
);
}
}
+
+class _SelectedCounts {
+ final int branches;
+ final int users;
+
+ _SelectedCounts(this.branches, this.users);
+}