add flutter

This commit is contained in:
Ariska
2026-03-11 15:29:37 +07:00
parent c253e1a370
commit 619d758027
9490 changed files with 135801 additions and 1353 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: android
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+35
View File
@@ -0,0 +1,35 @@
# Ontime User (Flutter)
Flutter app for Ontime **user** (pelanggan), using backend API at `https://apitest.semestaterpadu.my.id/api/`.
## Setup
- Flutter SDK (e.g. 3.19+)
- Android: run `flutter create . --platforms=android` if the project was scaffolded without platform folders.
- API uses HTTP Basic Auth; configure in backendpanel if required.
## Run / Build
```bash
flutter pub get
flutter run
# or
flutter build apk --release
```
APK output: `build/app/outputs/flutter-apk/app-release.apk`.
## Firebase (FCM)
To enable push notifications:
1. Add `google-services.json` to `android/app/`.
2. Ensure `android/build.gradle` and `android/app/build.gradle` apply the Google services plugin.
If Firebase is not configured, the app still runs; FCM token is sent to the backend on login when available.
## Validation
- **Login**: `Pelanggan/login` (email or no_telepon + password, optional token).
- **Home**: `Pelanggan/home` (slider, fitur, merchant nearby).
- Compare with native app `OnTime_User_live` and backend responses for parity.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.ontime_user_flutter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ontime_user_flutter"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ontime_user_flutter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.ontime_user_flutter
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
@@ -0,0 +1,18 @@
class AppConfig {
/// Base URL for the Ontime test backend.
static const String baseUrl = 'https://apitest.semestaterpadu.my.id/';
/// API root (`/api/`).
static const String apiBase = '${baseUrl}api/';
/// Image base URLs (kept consistent with Java Constants).
static const String imagesFitur = '${baseUrl}images/fitur/';
static const String imagesMerchant = '${baseUrl}images/merchant/';
static const String imagesBank = '${baseUrl}images/bank/';
static const String imagesItem = '${baseUrl}images/itemmerchant/';
static const String imagesBerita = '${baseUrl}images/berita/';
static const String imagesSlider = '${baseUrl}images/promo/';
static const String imagesDriver = '${baseUrl}images/fotodriver/';
static const String imagesUser = '${baseUrl}images/pelanggan/';
}
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ontime_user_flutter/features/auth/presentation/login_screen.dart';
import 'package:ontime_user_flutter/features/home/presentation/home_shell.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'rootNavigator');
GoRouter createAppRouter() {
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/auth/login',
routes: [
GoRoute(
path: '/auth/login',
name: 'login',
pageBuilder: (context, state) =>
const MaterialPage(child: LoginScreen()),
),
GoRoute(
path: '/home',
name: 'home',
pageBuilder: (context, state) =>
const MaterialPage(child: HomeShell()),
),
],
);
}
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData light() {
final base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF00AEEF),
),
scaffoldBackgroundColor: Colors.grey[50],
appBarTheme: const AppBarTheme(
elevation: 0,
centerTitle: true,
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
);
}
}
@@ -0,0 +1,32 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
/// FCM token for backend (Pelanggan uses `token` or `reg_id`).
/// Initialize in main(), then pass token to login.
class FcmService {
static bool _initialized = false;
static Future<void> init() async {
if (_initialized) return;
try {
await Firebase.initializeApp();
_initialized = true;
} catch (_) {
// No Firebase config (e.g. missing google-services.json)
}
}
static Future<String?> getToken() async {
if (!_initialized) return null;
try {
final token = await FirebaseMessaging.instance.getToken();
return token;
} catch (_) {
return null;
}
}
static void setBackgroundHandler(Future<void> Function(RemoteMessage) handler) {
FirebaseMessaging.onBackgroundMessage(handler);
}
}
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import 'package:ontime_user_flutter/core/app_config.dart';
class ApiClient {
ApiClient({
required String? basicAuthUser,
required String? basicAuthPassword,
}) : _dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBase,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 20),
),
) {
if (basicAuthUser != null && basicAuthPassword != null) {
final auth = '$basicAuthUser:$basicAuthPassword';
final encoded = String.fromCharCodes(auth.codeUnits);
_dio.options.headers['Authorization'] = 'Basic $encoded';
}
}
final Dio _dio;
Dio get raw => _dio;
}
@@ -0,0 +1,71 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ontime_user_flutter/data/api/api_client.dart';
import 'package:ontime_user_flutter/features/auth/data/auth_api.dart';
import 'package:ontime_user_flutter/features/auth/data/models/user_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthState {
const AuthState({
this.user,
this.isLoading = false,
this.errorMessage,
});
final UserModel? user;
final bool isLoading;
final String? errorMessage;
AuthState copyWith({
UserModel? user,
bool? isLoading,
String? errorMessage,
}) {
return AuthState(
user: user ?? this.user,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
final authControllerProvider =
StateNotifierProvider<AuthController, AuthState>((ref) {
return AuthController();
});
class AuthController extends StateNotifier<AuthState> {
AuthController() : super(const AuthState());
late final AuthApi _api =
AuthApi(ApiClient(basicAuthUser: null, basicAuthPassword: null));
Future<void> login({
String? phone,
String? email,
required String password,
String? fcmToken,
}) async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
final user = await _api.login(
phone: phone,
email: email,
password: password,
fcmToken: fcmToken,
);
state = state.copyWith(user: user, isLoading: false);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_id', user.id);
await prefs.setString('user_token', user.token);
await prefs.setString('user_phone', user.phone);
await prefs.setString('user_email', user.email);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
}
}
}
@@ -0,0 +1,47 @@
import 'package:dio/dio.dart';
import 'package:ontime_user_flutter/data/api/api_client.dart';
import 'package:ontime_user_flutter/features/auth/data/models/user_model.dart';
class AuthApi {
AuthApi(this._client);
final ApiClient _client;
Future<UserModel> login({
String? phone,
String? email,
required String password,
String? fcmToken,
}) async {
final payload = <String, dynamic>{
'password': password,
};
if (phone != null && phone.isNotEmpty) {
payload['no_telepon'] = phone;
}
if (email != null && email.isNotEmpty) {
payload['email'] = email;
}
if (fcmToken != null && fcmToken.isNotEmpty) {
payload['token'] = fcmToken;
}
final Response<dynamic> response =
await _client.raw.post('Pelanggan/login', data: payload);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['code']?.toString() != '200') {
throw Exception(data['message'] ?? 'Login failed');
}
final List<dynamic> list = data['data'] as List<dynamic>? ?? [];
if (list.isEmpty) {
throw Exception('User data not found in response');
}
return UserModel.fromJson(list.first as Map<String, dynamic>);
}
}
@@ -0,0 +1,35 @@
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
@JsonSerializable()
class UserModel {
UserModel({
required this.id,
required this.fullName,
required this.email,
required this.phone,
required this.token,
});
@JsonKey(name: 'id')
final String id;
@JsonKey(name: 'fullnama')
final String fullName;
@JsonKey(name: 'email')
final String email;
@JsonKey(name: 'no_telepon')
final String phone;
@JsonKey(name: 'token')
final String token;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
}
@@ -0,0 +1,22 @@
// GENERATED CODE - placeholder for build_runner. You can regenerate.
part of 'user_model.dart';
UserModel _$UserModelFromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String? ?? '',
fullName: json['fullnama'] as String? ?? '',
email: json['email'] as String? ?? '',
phone: json['no_telepon'] as String? ?? '',
token: json['token'] as String? ?? '',
);
}
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'id': instance.id,
'fullnama': instance.fullName,
'email': instance.email,
'no_telepon': instance.phone,
'token': instance.token,
};
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ontime_user_flutter/core/fcm_service.dart';
import 'package:ontime_user_flutter/features/auth/application/auth_controller.dart';
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_phoneController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
final fcmToken = await FcmService.getToken();
final auth = ref.read(authControllerProvider.notifier);
await auth.login(
email: _emailController.text.trim().isEmpty
? null
: _emailController.text.trim(),
phone: _phoneController.text.trim().isEmpty
? null
: _phoneController.text.trim(),
password: _passwordController.text,
fcmToken: fcmToken,
);
final state = ref.read(authControllerProvider);
if (state.user != null) {
if (mounted) {
context.go('/home');
}
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(authControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Masuk'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
),
const SizedBox(height: 12),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'No. Telepon',
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Kata sandi',
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password wajib diisi';
}
return null;
},
),
const SizedBox(height: 16),
if (state.errorMessage != null)
Text(
state.errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
const Spacer(),
ElevatedButton(
onPressed: state.isLoading ? null : _submit,
child: state.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Masuk'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
@override
Widget build(BuildContext context) {
final pages = <Widget>[
const Center(child: Text('Beranda (slider, fitur, merchant terdekat)')),
const Center(child: Text('Pesanan (aktif & riwayat)')),
const Center(child: Text('Dompet (saldo & topup/withdraw)')),
const Center(child: Text('Profil')),
];
return Scaffold(
body: SafeArea(
child: pages[_index],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Beranda',
),
NavigationDestination(
icon: Icon(Icons.receipt_long_outlined),
selectedIcon: Icon(Icons.receipt_long),
label: 'Pesanan',
),
NavigationDestination(
icon: Icon(Icons.account_balance_wallet_outlined),
selectedIcon: Icon(Icons.account_balance_wallet),
label: 'Dompet',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profil',
),
],
),
);
}
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/app_router.dart';
import 'core/app_theme.dart';
import 'core/fcm_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FcmService.init();
final router = createAppRouter();
runApp(ProviderScope(child: OntimeUserApp(router: router)));
}
class OntimeUserApp extends StatelessWidget {
const OntimeUserApp({super.key, required this.router});
final Object router;
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Ontime User',
theme: AppTheme.light(),
routerConfig: router as dynamic,
);
}
}
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
name: ontime_user_flutter
description: Ontime user Flutter app (MVP) consuming backendpanel Pelanggan API.
version: 0.1.0+1
environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# HTTP & JSON
dio: ^5.7.0
json_annotation: ^4.9.0
# State management
flutter_riverpod: ^2.6.1
# Navigation
go_router: ^14.3.0
# Storage
shared_preferences: ^2.3.3
# Location & maps (MVP)
geolocator: ^13.0.2
google_maps_flutter: ^2.10.0
permission_handler: ^11.3.1
# Firebase / FCM (tokens wired to existing backend)
firebase_core: ^3.6.0
firebase_messaging: ^15.1.0
# UI helpers
intl: ^0.19.0
cached_network_image: ^3.4.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
build_runner: ^2.4.13
json_serializable: ^6.9.0
flutter:
uses-material-design: true
assets:
- assets/images/
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ontime_user_flutter/core/app_router.dart';
import 'package:ontime_user_flutter/main.dart';
void main() {
testWidgets('App loads and shows login', (WidgetTester tester) async {
final router = createAppRouter();
await tester.pumpWidget(
ProviderScope(
child: OntimeUserApp(router: router),
),
);
await tester.pumpAndSettle();
expect(find.text('Masuk'), findsOneWidget);
});
}