diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..d8d8067
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8367b9a
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..b3eca56
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a5d65de
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4cd47b2
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c79c58a
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..d4ab59b 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..f74d0ff 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..edb2e51 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..bd3a345 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..c668430 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..e3f9609
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #EE322F
+
\ No newline at end of file
diff --git a/assets/svg/icons/github.svg b/assets/svg/icons/github.svg
new file mode 100644
index 0000000..c61cf48
--- /dev/null
+++ b/assets/svg/icons/github.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/lib/core/constants/asset_paths.dart b/lib/core/constants/asset_paths.dart
index 8260882..84f8e1a 100644
--- a/lib/core/constants/asset_paths.dart
+++ b/lib/core/constants/asset_paths.dart
@@ -19,4 +19,5 @@ abstract final class AssetPaths {
static const iconUtensils = 'assets/svg/icons/utensils.svg';
static const iconFine = 'assets/svg/icons/circle-dollar-sign.svg';
static const iconHistory = 'assets/svg/icons/history.svg';
+ static const iconGithub = 'assets/svg/icons/github.svg';
}
diff --git a/lib/features/home/home_strings.dart b/lib/features/home/home_strings.dart
index b6d565f..82dde68 100644
--- a/lib/features/home/home_strings.dart
+++ b/lib/features/home/home_strings.dart
@@ -20,4 +20,7 @@ abstract final class HomeStrings {
static const restaurantShortcut = 'Central (Meléndez)';
static const comingSoon = 'Muy pronto podrás usar esta función.';
static const allFunctionalitiesTitle = 'Todas las funcionalidades';
+ static const repositoryLabel = 'code3743/univalle_app';
+ static const repositoryUrl = 'https://github.com/code3743/univalle_app';
+ static const cannotOpenRepositoryLink = 'No pudimos abrir el repositorio.';
}
diff --git a/lib/features/home/presentation/providers/app_version_provider.dart b/lib/features/home/presentation/providers/app_version_provider.dart
new file mode 100644
index 0000000..0d16587
--- /dev/null
+++ b/lib/features/home/presentation/providers/app_version_provider.dart
@@ -0,0 +1,11 @@
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'app_version_provider.g.dart';
+
+/// App version shown in the Home footer, e.g. "0.1.0".
+@riverpod
+Future appVersion(Ref ref) async {
+ final info = await PackageInfo.fromPlatform();
+ return info.version;
+}
diff --git a/lib/features/home/presentation/views/home_view.dart b/lib/features/home/presentation/views/home_view.dart
index b0f4a5d..953b642 100644
--- a/lib/features/home/presentation/views/home_view.dart
+++ b/lib/features/home/presentation/views/home_view.dart
@@ -17,6 +17,7 @@ import '../../../auth/presentation/viewmodels/auth_view_model.dart';
import '../../../profile/presentation/viewmodels/profile_view_model.dart';
import '../../home_strings.dart';
import '../providers/latest_semester_provider.dart';
+import '../widgets/home_footer.dart';
import '../widgets/home_top_bar.dart';
import '../widgets/program_card.dart';
import '../widgets/quick_access_items.dart';
@@ -145,6 +146,8 @@ class HomeView extends ConsumerWidget {
)
.toList(),
),
+ const SizedBox(height: AppSpacing.lg),
+ const HomeFooter(),
],
),
),
diff --git a/lib/features/home/presentation/widgets/home_footer.dart b/lib/features/home/presentation/widgets/home_footer.dart
new file mode 100644
index 0000000..80f4386
--- /dev/null
+++ b/lib/features/home/presentation/widgets/home_footer.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+import '../../../../core/constants/asset_paths.dart';
+import '../../../../core/extensions/snackbar_extension.dart';
+import '../../home_strings.dart';
+import '../providers/app_version_provider.dart';
+
+class HomeFooter extends ConsumerWidget {
+ const HomeFooter({super.key});
+
+ Future _openRepository(BuildContext context) async {
+ final uri = Uri.parse(HomeStrings.repositoryUrl);
+ final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
+ if (!launched && context.mounted) {
+ context.showSnack(HomeStrings.cannotOpenRepositoryLink);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final colorScheme = Theme.of(context).colorScheme;
+ final mutedStyle = Theme.of(context).textTheme.bodySmall
+ ?.copyWith(color: colorScheme.onSurfaceVariant);
+ final version = ref.watch(appVersionProvider).value;
+
+ return Center(
+ child: Column(
+ children: [
+ InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: () => _openRepository(context),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ AssetPaths.iconGithub,
+ width: 15,
+ height: 15,
+ colorFilter: ColorFilter.mode(
+ colorScheme.onSurfaceVariant,
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(HomeStrings.repositoryLabel, style: mutedStyle),
+ ],
+ ),
+ ),
+ ),
+ if (version != null) Text('v$version', style: mutedStyle),
+ ],
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 34e9cec..cf0864c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -265,6 +265,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
+ ffi_leak_tracker:
+ dependency: transitive
+ description:
+ name: ffi_leak_tracker
+ sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2"
file:
dependency: transitive
description:
@@ -589,6 +597,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ package_info_plus:
+ dependency: "direct main"
+ description:
+ name: package_info_plus
+ sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.2.1"
+ package_info_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: package_info_plus_platform_interface
+ sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
path:
dependency: transitive
description:
@@ -963,7 +987,7 @@ packages:
source: hosted
version: "3.2.6"
url_launcher_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
@@ -1098,6 +1122,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.26.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.0"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a7931f9..0f8c5ad 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -20,6 +20,7 @@ dependencies:
google_fonts: ^8.2.1
html: ^0.15.7
intl: ^0.20.3
+ package_info_plus: ^10.2.1
riverpod_annotation: ^4.0.7
shared_preferences: ^2.5.5
url_launcher: ^6.3.1
@@ -33,6 +34,7 @@ dev_dependencies:
build_runner: ^2.16.1
flutter_native_splash: ^2.4.8
mocktail: ^1.0.4
+ url_launcher_platform_interface: ^2.3.2
flutter:
uses-material-design: true
diff --git a/test/features/home/presentation/providers/app_version_provider_test.dart b/test/features/home/presentation/providers/app_version_provider_test.dart
new file mode 100644
index 0000000..0468a8a
--- /dev/null
+++ b/test/features/home/presentation/providers/app_version_provider_test.dart
@@ -0,0 +1,25 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:univalle_app/features/home/presentation/providers/app_version_provider.dart';
+
+import '../../../../helpers/container.dart';
+
+void main() {
+ setUp(() {
+ PackageInfo.setMockInitialValues(
+ appName: 'Univalle App',
+ packageName: 'co.edu.univalle.univalle_app',
+ version: '1.2.3',
+ buildNumber: '7',
+ buildSignature: '',
+ );
+ });
+
+ test('reads the version reported by the platform', () async {
+ final container = createContainer();
+
+ final version = await container.read(appVersionProvider.future);
+
+ expect(version, '1.2.3');
+ });
+}
diff --git a/test/features/home/presentation/widgets/home_footer_test.dart b/test/features/home/presentation/widgets/home_footer_test.dart
new file mode 100644
index 0000000..713aa27
--- /dev/null
+++ b/test/features/home/presentation/widgets/home_footer_test.dart
@@ -0,0 +1,73 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:univalle_app/features/home/home_strings.dart';
+import 'package:univalle_app/features/home/presentation/widgets/home_footer.dart';
+import 'package:url_launcher_platform_interface/link.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import '../../../../helpers/pump_app.dart';
+
+class _FakeUrlLauncher extends UrlLauncherPlatform {
+ _FakeUrlLauncher({this.result = true});
+
+ final bool result;
+ String? launchedUrl;
+
+ @override
+ LinkDelegate? get linkDelegate => null;
+
+ @override
+ Future launchUrl(String url, LaunchOptions options) async {
+ launchedUrl = url;
+ return result;
+ }
+}
+
+void main() {
+ setUp(() {
+ PackageInfo.setMockInitialValues(
+ appName: 'Univalle App',
+ packageName: 'co.edu.univalle.univalle_app',
+ version: '1.2.3',
+ buildNumber: '7',
+ buildSignature: '',
+ );
+ });
+
+ testWidgets('shows the repository label and the app version', (tester) async {
+ UrlLauncherPlatform.instance = _FakeUrlLauncher();
+
+ await tester.pumpApp(const HomeFooter());
+ await tester.pumpAndSettle();
+
+ expect(find.text(HomeStrings.repositoryLabel), findsOneWidget);
+ expect(find.text('v1.2.3'), findsOneWidget);
+ });
+
+ testWidgets('opens the repository url when tapped', (tester) async {
+ final fakeLauncher = _FakeUrlLauncher();
+ UrlLauncherPlatform.instance = fakeLauncher;
+
+ await tester.pumpApp(const HomeFooter());
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text(HomeStrings.repositoryLabel));
+ await tester.pumpAndSettle();
+
+ expect(fakeLauncher.launchedUrl, HomeStrings.repositoryUrl);
+ });
+
+ testWidgets('shows a snackbar when the link cannot be opened', (
+ tester,
+ ) async {
+ UrlLauncherPlatform.instance = _FakeUrlLauncher(result: false);
+
+ await tester.pumpApp(const HomeFooter());
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text(HomeStrings.repositoryLabel));
+ await tester.pumpAndSettle();
+
+ expect(find.text(HomeStrings.cannotOpenRepositoryLink), findsOneWidget);
+ });
+}