diff --git a/lib/main.dart b/lib/main.dart index 0277a02..873ec4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,10 +12,9 @@ void main() async { await dotenv.load(fileName: "server/.env.dev"); HttpOverrides.global = MyHttpOverrides(); WidgetsFlutterBinding.ensureInitialized(); - await initializeDateFormatting( - 'id_ID', - null, - ).then((_) => runApp(const MyApp())); + await initializeDateFormatting('id_ID', null).then((_) { + runApp(const MyApp()); + }); } class MyApp extends StatelessWidget { @@ -23,16 +22,17 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return ScreenUtilInit( - designSize: const Size(375, 812), - builder: - (_, child) => ChangeNotifierProvider( - create: (_) => UserViewModel(), - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: router, - ), - ), + return ChangeNotifierProvider( + create: (_) => AuthViewModel(), + child: ScreenUtilInit( + designSize: const Size(375, 812), + builder: (_, child) { + return MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: router, + ); + }, + ), ); } } diff --git a/lib/model/auth_model.dart b/lib/model/auth_model.dart index e205151..92c1640 100644 --- a/lib/model/auth_model.dart +++ b/lib/model/auth_model.dart @@ -1,3 +1,5 @@ +import 'package:rijig_mobile/core/api_services.dart'; + class AuthModel { final int status; final String message; @@ -6,8 +8,35 @@ class AuthModel { factory AuthModel.fromJson(Map json) { return AuthModel( - status: json['meta']['status'], - message: json['meta']['message'], + status: json['meta']?['status'] ?? 0, + message: json['meta']?['message'] ?? '', ); } } + +class AuthService { + final ApiService _apiService = ApiService(); + + Future login(String phone) async { + try { + var response = await _apiService.post('/authmasyarakat/auth', { + 'phone': phone, + }); + return AuthModel.fromJson(response); + } catch (e) { + rethrow; + } + } + + Future> verifyOtp(String phone, String otp) async { + try { + var response = await _apiService.post('/authmasyarakat/verify-otp', { + 'phone': phone, + 'otp': otp, + }); + return response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/screen/app/profil/profil_screen.dart b/lib/screen/app/profil/profil_screen.dart index 8c68628..8e3f7a0 100644 --- a/lib/screen/app/profil/profil_screen.dart +++ b/lib/screen/app/profil/profil_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ProfilScreen extends StatefulWidget { const ProfilScreen({super.key}); @@ -12,7 +14,22 @@ class _ProfilScreenState extends State { Widget build(BuildContext context) { final titleofscreen = "Profil"; return Scaffold( - body: Center(child: Text("ini adalah halaman $titleofscreen")), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("ini adalah halaman $titleofscreen"), + TextButton( + onPressed: () async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove('isLoggedIn'); + router.go('/login'); + }, + child: Text("keluar"), + ), + ], + ), + ), ); } } diff --git a/lib/screen/auth/login_screen.dart b/lib/screen/auth/login_screen.dart index e3cfcbf..44232ab 100644 --- a/lib/screen/auth/login_screen.dart +++ b/lib/screen/auth/login_screen.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:rijig_mobile/core/guide.dart'; import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; +import 'package:rijig_mobile/widget/buttoncard.dart'; +import 'package:rijig_mobile/widget/formfiled.dart'; class LoginScreen extends StatelessWidget { final _phoneController = TextEditingController(); @@ -11,52 +14,62 @@ class LoginScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Login')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Consumer( - builder: (context, userVM, child) { - if (userVM.authModel?.status == 200) { - Future.delayed(Duration.zero, () { - router.go('/verif-otp', extra: _phoneController.text); - }); - } + body: SafeArea( + child: Padding( + padding: PaddingCustom().paddingHorizontalVertical(15, 30), + child: Consumer( + builder: (context, userVM, child) { + if (userVM.authModel?.status == 200) { + Future.delayed(Duration.zero, () { + router.go('/verif-otp', extra: _phoneController.text); + }); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - controller: _phoneController, - keyboardType: TextInputType.phone, - decoration: InputDecoration( - labelText: 'Phone Number', + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FormFieldOne( + controllers: _phoneController, + hintText: 'Phone Number', + isRequired: true, + onTap: () {}, + keyboardType: TextInputType.phone, errorText: userVM.errorMessage, ), - ), - SizedBox(height: 20), - userVM.isLoading - ? CircularProgressIndicator() - : ElevatedButton( - onPressed: () { - if (_phoneController.text.isNotEmpty) { - userVM.login(_phoneController.text); - } - }, - child: Text('Send OTP'), + SizedBox(height: 20), + + userVM.isLoading + ? CircularProgressIndicator() + : CardButtonOne( + textButton: 'Send OTP', + fontSized: 16, + colorText: Colors.white, + borderRadius: 12, + horizontal: double.infinity, + vertical: 50, + onTap: () { + if (_phoneController.text.isNotEmpty) { + userVM.login(_phoneController.text); + } + }, + loadingTrue: userVM.isLoading, + usingRow: false, + ), + + if (userVM.authModel != null) + Text( + userVM.authModel!.message, + style: TextStyle( + color: + userVM.authModel!.status == 200 + ? Colors.green + : Colors.red, + ), ), - if (userVM.authModel != null) - Text( - userVM.authModel!.message, - style: TextStyle( - color: - userVM.authModel!.status == 200 - ? Colors.green - : Colors.red, - ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ); diff --git a/lib/screen/auth/otp_screen.dart b/lib/screen/auth/otp_screen.dart index 7e99c4f..4256f97 100644 --- a/lib/screen/auth/otp_screen.dart +++ b/lib/screen/auth/otp_screen.dart @@ -1,66 +1,80 @@ import 'package:flutter/material.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; +import 'package:rijig_mobile/widget/buttoncard.dart'; -class VerifotpScreen extends StatelessWidget { - final String phone; +class VerifotpScreen extends StatefulWidget { + final dynamic phone; + + const VerifotpScreen({super.key, required this.phone}); + + @override + State createState() => _VerifotpScreenState(); +} + +class _VerifotpScreenState extends State { final _otpController = TextEditingController(); - VerifotpScreen({super.key, required this.phone}); - @override Widget build(BuildContext context) { + final viewModel = Provider.of(context); return Scaffold( - appBar: AppBar(title: Text('Verify OTP')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Consumer( - builder: (context, userVM, child) { - if (userVM.isLoading) { - return Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Phone: ${widget.phone}', style: TextStyle(fontSize: 18)), + SizedBox(height: 20), - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Phone: $phone', style: TextStyle(fontSize: 18)), - SizedBox(height: 20), - - TextField( - controller: _otpController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Enter OTP', - errorText: userVM.errorMessage, - ), + PinCodeTextField( + controller: _otpController, + length: 4, + onChanged: (value) {}, + appContext: context, + keyboardType: TextInputType.number, + autoFocus: true, + pinTheme: PinTheme( + shape: PinCodeFieldShape.box, + borderRadius: BorderRadius.circular(5), + fieldHeight: 50, + fieldWidth: 50, + inactiveColor: Colors.black54, + activeColor: Colors.blue, + selectedColor: Colors.blue, ), - SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - String otp = _otpController.text; - - if (otp.isNotEmpty) { - await Future.delayed(Duration(milliseconds: 50)); - userVM.verifyOtp(phone, otp); - - if (userVM.authModel?.status == 200) { - debugPrint("routing ke halaman home"); - router.go('/navigasi'); - } - } - }, - child: Text('Verify OTP'), - ), - - if (userVM.errorMessage != null) - Text( - userVM.errorMessage!, - style: TextStyle(color: Colors.red), - ), - ], - ); - }, + animationType: AnimationType.fade, + textStyle: TextStyle(fontSize: 20, color: Colors.black), + ), + SizedBox(height: 20), + CardButtonOne( + textButton: 'Verify OTP', + fontSized: 16, + colorText: Colors.white, + borderRadius: 12, + horizontal: double.infinity, + vertical: 50, + onTap: () { + if (_otpController.text.isNotEmpty) { + viewModel.verifyOtp(widget.phone, _otpController.text).then( + (_) { + if (viewModel.errorMessage == null) { + router.go('/navigasi'); + } else { + debugPrint(viewModel.errorMessage ?? ''); + } + }, + ); + } + }, + loadingTrue: viewModel.isLoading, + usingRow: false, + ), + ], + ), ), ), ); diff --git a/lib/viewmodel/auth_vmod.dart b/lib/viewmodel/auth_vmod.dart index 8125d94..bb85b6d 100644 --- a/lib/viewmodel/auth_vmod.dart +++ b/lib/viewmodel/auth_vmod.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:rijig_mobile/core/api_services.dart'; import 'package:rijig_mobile/model/auth_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class UserViewModel extends ChangeNotifier { - final ApiService _apiService = ApiService(); +class AuthViewModel extends ChangeNotifier { + final AuthService _authService = AuthService(); bool isLoading = false; String? errorMessage; AuthModel? authModel; @@ -15,22 +14,13 @@ class UserViewModel extends ChangeNotifier { errorMessage = null; notifyListeners(); - var response = await _apiService.post('/authmasyarakat/auth', { - 'phone': phone, - }); + authModel = await _authService.login(phone); - authModel = AuthModel.fromJson(response); - - if (authModel?.status == 200) { - } else { + if (authModel?.status != 200) { errorMessage = authModel?.message ?? 'Failed to send OTP'; } } catch (e) { - if (e is NetworkException) { - errorMessage = e.message; - } else { - errorMessage = 'Something went wrong. Please try again later.'; - } + errorMessage = 'Error: $e'; } finally { isLoading = false; notifyListeners(); @@ -43,28 +33,21 @@ class UserViewModel extends ChangeNotifier { errorMessage = null; notifyListeners(); - var response = await _apiService.post('/authmasyarakat/verify-otp', { - 'phone': phone, - 'otp': otp, - }); + var response = await _authService.verifyOtp(phone, otp); - if (response['meta']['status'] == 200) { + if (response['meta'] != null && response['meta']['status'] == 200) { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString('token', response['data']['token']); await prefs.setString('user_id', response['data']['user_id']); await prefs.setString('user_role', response['data']['user_role']); await prefs.setBool('isLoggedIn', true); - debugPrint("berhasil login"); + authModel = AuthModel.fromJson(response['data']); } else { - errorMessage = response['meta']['message'] ?? 'Failed to verify OTP'; + errorMessage = response['meta']?['message'] ?? 'Failed to verify OTP'; } } catch (e) { - if (e is NetworkException) { - errorMessage = e.message; - } else { - errorMessage = 'Something went wrong. Please try again later.'; - } + errorMessage = 'Error: $e'; } finally { isLoading = false; notifyListeners(); diff --git a/lib/widget/buttoncard.dart b/lib/widget/buttoncard.dart new file mode 100644 index 0000000..0883dbf --- /dev/null +++ b/lib/widget/buttoncard.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:rijig_mobile/core/guide.dart'; + +class CardButtonOne extends StatelessWidget { + final String textButton; + final double fontSized; + final Color colorText; + final double borderRadius; + final double horizontal; + final double vertical; + final Function() onTap; + final Color? color; + final BoxBorder? borderAll; + final bool loadingTrue; + final bool usingRow; + final MainAxisSize mainAxisSize; + final Widget? child; + const CardButtonOne({ + super.key, + required this.textButton, + required this.fontSized, + required this.colorText, + required this.borderRadius, + required this.horizontal, + required this.vertical, + required this.onTap, + this.loadingTrue = false, + this.usingRow = false, + this.color, + this.borderAll, + this.mainAxisSize = MainAxisSize.max, + this.child, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: vertical, + width: horizontal, + decoration: BoxDecoration( + color: color ?? blackNavyColor, + borderRadius: BorderRadius.circular(borderRadius).w, + border: borderAll, + ), + margin: EdgeInsets.zero, + child: Center( + child: + (!loadingTrue) + ? usingRow == false + ? Text( + textButton, + style: GoogleFonts.roboto( + textStyle: TextStyle( + fontWeight: bold, + fontSize: fontSized.sp, + color: colorText, + ), + ), + ) + : Row( + mainAxisSize: mainAxisSize, + children: [ + Text( + textButton, + style: GoogleFonts.roboto( + textStyle: TextStyle( + fontWeight: medium, + fontSize: fontSized.sp, + color: colorText, + ), + ), + ), + GapCustom().gapValue(10, false), + Container(child: child), + ], + ) + : CircularProgressIndicator( + backgroundColor: whiteColor, + color: primaryColor, + ), + ), + ), + ); + } +} diff --git a/lib/widget/formfiled.dart b/lib/widget/formfiled.dart new file mode 100644 index 0000000..8b605dc --- /dev/null +++ b/lib/widget/formfiled.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:rijig_mobile/core/guide.dart'; + +class FormFieldOne extends StatefulWidget { + const FormFieldOne({ + super.key, + this.fontSize, + this.fontSizeField, + this.controllers, + this.hintText = '', + this.enabled = true, + required this.isRequired, + this.maxLines = 1, + this.minLines = 1, + this.keyboardType = TextInputType.text, + this.textInputAction = TextInputAction.next, + this.prefixIcon, + this.prefix, + this.errorText, + this.errorTextWidget, + this.suffixIcon, + this.inputColor, + this.textColor, + this.isObsecure = false, + this.isHasHint = true, + this.isPrice = false, + this.placeholder, + this.onChangeText, + required this.onTap, + this.isHandOver = false, + this.focusNode, + this.borderColor, + this.onEditingComplete, + this.hintTextStyle, + this.hintTextBoxFieldStyle, + this.typeFormat, + this.onChanged, + this.scrollPadding = const EdgeInsets.all(0), + this.onFieldSubmitted, + this.validator, + this.readOnly = false, + this.controllerTextStyle, + this.maxLength, + this.onFocus = false, + }); + + final void Function(String)? onChanged; + final TextEditingController? controllers; + final double? fontSize; + final double? fontSizeField; + final String hintText; + final String? placeholder; + final bool enabled; + final bool readOnly; + final bool isRequired; + final bool isObsecure; + final bool isHasHint; + final bool isPrice; + final bool isHandOver; + final int maxLines; + final int minLines; + final String? typeFormat; + final FocusNode? focusNode; + final TextInputAction textInputAction; + final TextInputType keyboardType; + final EdgeInsets scrollPadding; + final Widget? prefixIcon; + final Widget? prefix; + final String? errorText; + final Widget? suffixIcon; + final Widget? errorTextWidget; + final Color? inputColor; + final Color? borderColor; + final Color? textColor; + final Function? onChangeText; + final Function onTap; + final Function? onEditingComplete; + final TextStyle? hintTextStyle; + final TextStyle? hintTextBoxFieldStyle; + final TextStyle? controllerTextStyle; + final Function(String)? onFieldSubmitted; + final FormFieldValidator? validator; + final int? maxLength; + final bool onFocus; + + @override + State createState() => _FormFieldOneState(); +} + +class _FormFieldOneState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + + _focusNode = + widget.isRequired ? (widget.focusNode ?? FocusNode()) : FocusNode(); + } + + @override + void dispose() { + if (widget.controllers != null) { + widget.controllers?.dispose(); + } + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (widget.isHasHint) + widget.isHasHint == true && + widget.isRequired == true && + widget.hintText != '' + ? Container( + margin: const EdgeInsets.fromLTRB(0, 12, 16, 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: RichText( + text: TextSpan( + style: GoogleFonts.dmSans( + fontSize: widget.fontSize ?? 14.sp, + color: blackNavyColor, + ), + children: [ + TextSpan( + text: widget.hintText, + style: widget.hintTextStyle, + ), + TextSpan( + text: '\t*', + style: GoogleFonts.dmSans(color: redColor), + ), + ], + ), + ), + ), + ], + ), + ) + : widget.isHasHint && widget.hintText != '' + ? Container( + padding: PaddingCustom().paddingOnly( + left: 0, + top: 12, + bottom: 4, + ), + alignment: Alignment.centerLeft, + child: Text(widget.hintText, style: widget.hintTextStyle), + ) + : const SizedBox(height: 8), + Card( + margin: const EdgeInsets.all(0.0), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: TextFormField( + readOnly: widget.readOnly, + scrollPadding: widget.scrollPadding, + minLines: widget.minLines, + textInputAction: widget.textInputAction, + focusNode: _focusNode, + onFieldSubmitted: widget.onFieldSubmitted, + controller: widget.controllers, + onChanged: widget.onChanged, + maxLength: widget.maxLength, + onTap: () { + widget.onTap(); + }, + onEditingComplete: + widget.onEditingComplete != null + ? () { + widget.onEditingComplete!(); + } + : null, + enabled: widget.enabled, + obscureText: widget.isObsecure, + style: + widget.controllerTextStyle ?? + GoogleFonts.dmSans( + fontSize: widget.fontSizeField ?? 12.sp, + fontWeight: regular, + color: + widget.textColor ?? + (widget.enabled ? Colors.black : Colors.black54), + ), + keyboardType: widget.keyboardType, + maxLines: widget.maxLines, + decoration: InputDecoration( + isDense: true, + fillColor: widget.inputColor ?? whiteColor, + filled: true, + hintText: widget.placeholder ?? 'Masukan ${widget.hintText}', + hintStyle: + widget.hintTextBoxFieldStyle ?? + GoogleFonts.dmSans( + fontSize: widget.fontSize ?? 14.sp, + fontWeight: regular, + color: + widget.textColor ?? + (widget.enabled ? Colors.black54 : Colors.black38), + ), + prefixIcon: widget.prefixIcon, + prefix: widget.prefix, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: blackNavyColor, width: 0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: widget.borderColor ?? blackNavyColor, + width: 0.5, + ), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: blackNavyColor, width: 0.5), + ), + focusedBorder: + widget.isRequired + ? OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: primaryColor, width: 0.5), + ) + : OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: blackNavyColor, + width: 0.5, + ), + ), + suffixIcon: widget.suffixIcon, + ), + validator: + widget.validator ?? + (value) { + if (value == null || value.isEmpty) { + return 'Please enter some text'; + } + return null; + }, + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9ef4610..6c6d5cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -344,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" + url: "https://pub.dev" + source: hosted + version: "8.0.1" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 33d88ca..f669b4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: http: ^1.3.0 iconsax_flutter: ^1.0.0 intl: ^0.20.2 + pin_code_fields: ^8.0.1 provider: ^6.1.4 shared_preferences: ^2.3.3