connecting API to forgot password, otp screen and reset password, along with making API files into 1 single file

This commit is contained in:
Samsularifin01 2026-05-09 01:07:30 +07:00
parent 62bc1cb0e2
commit 650e17012f
11 changed files with 342 additions and 42 deletions

View File

@ -0,0 +1,9 @@
class ForgotPasswordRequest {
final String email;
const ForgotPasswordRequest({required this.email});
Map<String, dynamic> toJson() {
return {"email": email};
}
}

View File

@ -0,0 +1,25 @@
class ForgotPasswordResponse {
final String status;
final String message;
final String? token;
final int? expired;
const ForgotPasswordResponse({
required this.status,
required this.message,
this.token,
this.expired,
});
factory ForgotPasswordResponse.fromJson(Map<String, dynamic> json) {
final dynamic expiredValue = json["expired"];
return ForgotPasswordResponse(
status: json["status"]?.toString() ?? "error",
message: json["message"]?.toString() ?? "",
token: json["token"]?.toString(),
expired: expiredValue is num ? expiredValue.toInt() : null,
);
}
bool get isSuccess => status.toLowerCase() == "success";
}

View File

@ -0,0 +1,19 @@
class ResetPasswordRequest {
final String email;
final String token;
final String password;
const ResetPasswordRequest({
required this.email,
required this.token,
required this.password,
});
Map<String, dynamic> toJson() {
return {
"email": email,
"token": token,
"password": password,
};
}
}

View File

@ -0,0 +1,18 @@
class ResetPasswordResponse {
final String status;
final String message;
const ResetPasswordResponse({
required this.status,
required this.message,
});
factory ResetPasswordResponse.fromJson(Map<String, dynamic> json) {
return ResetPasswordResponse(
status: json["status"]?.toString() ?? "error",
message: json["message"]?.toString() ?? "",
);
}
bool get isSuccess => status.toLowerCase() == "success";
}

View File

@ -2,17 +2,20 @@ import 'package:flutter/material.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart';
import '../services/tts_service.dart';
import '../services/forgot_password_api_service.dart';
import '../models/forgot_password_request.dart';
import '../utils/colors.dart';
import 'otp_screen.dart';
class ForgotPasswordScreen extends StatelessWidget {
final TextEditingController emailController = TextEditingController();
final TTSService tts = TTSService();
final ForgotPasswordApiService forgotApi = ForgotPasswordApiService();
ForgotPasswordScreen({super.key});
// ============ HANDLE RESET PASSWORD ============
void handleReset(BuildContext context) {
Future<void> handleReset(BuildContext context) async {
String email = emailController.text.trim();
// ============ VALIDASI EMAIL KOSONG ============
@ -27,11 +30,41 @@ class ForgotPasswordScreen extends StatelessWidget {
return;
}
tts.speak("OTP telah dikirim ke email Anda");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OTPScreen()),
final response = await forgotApi.requestReset(
ForgotPasswordRequest(email: email),
);
if (!context.mounted) {
return;
}
if (response.isSuccess) {
if (response.token == null || response.token!.isEmpty) {
tts.speak("Token OTP belum diterima dari server");
return;
}
if (response.expired == null) {
tts.speak("Waktu kedaluwarsa OTP belum diterima dari server");
return;
}
tts.speak(response.message.isNotEmpty
? response.message
: "OTP berhasil dikirim");
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OTPScreen(
email: email,
serverToken: response.token!,
expiredAt: response.expired!,
),
),
);
} else {
tts.speak(response.message.isNotEmpty
? response.message
: "Email tidak ditemukan");
}
}
@override

View File

@ -3,14 +3,40 @@ import 'package:flutter/services.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart';
import '../services/tts_service.dart';
import '../services/forgot_password_api_service.dart';
import '../models/forgot_password_request.dart';
import '../utils/colors.dart';
import 'reset_password_screen.dart';
class OTPScreen extends StatelessWidget {
class OTPScreen extends StatefulWidget {
final String email;
final String serverToken;
final int expiredAt;
const OTPScreen({
super.key,
required this.email,
required this.serverToken,
required this.expiredAt,
});
@override
State<OTPScreen> createState() => _OTPScreenState();
}
class _OTPScreenState extends State<OTPScreen> {
final TextEditingController otpController = TextEditingController();
final TTSService tts = TTSService();
final ForgotPasswordApiService forgotApi = ForgotPasswordApiService();
late String currentToken;
late int currentExpiredAt;
OTPScreen({super.key});
@override
void initState() {
super.initState();
currentToken = widget.serverToken;
currentExpiredAt = widget.expiredAt;
}
// ============ VERIFY OTP CODE ============
void verifyOTP(BuildContext context) {
@ -28,18 +54,73 @@ class OTPScreen extends StatelessWidget {
return;
}
// ============ LANGSUNG KE RESET PASSWORD (TANPA DATABASE) ============
// NOTE: Fitur ini bypass validasi karena belum ada database
final int nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
if (nowSeconds > currentExpiredAt) {
tts.speak("OTP sudah kedaluwarsa");
return;
}
if (otp != currentToken) {
tts.speak("OTP tidak sesuai");
return;
}
tts.speak("Kode OTP valid, lanjut ke reset password");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ResetPasswordScreen()),
MaterialPageRoute(
builder: (_) => ResetPasswordScreen(
email: widget.email,
token: currentToken,
),
),
);
}
Future<void> resendOtp() async {
final response = await forgotApi.requestReset(
ForgotPasswordRequest(email: widget.email),
);
if (!mounted) {
return;
}
if (response.isSuccess) {
if (response.token == null || response.token!.isEmpty) {
tts.speak("Token OTP belum diterima dari server");
return;
}
if (response.expired == null) {
tts.speak("Waktu kedaluwarsa OTP belum diterima dari server");
return;
}
setState(() {
currentToken = response.token!;
currentExpiredAt = response.expired!;
});
tts.speak(response.message.isNotEmpty
? response.message
: "OTP berhasil dikirim ulang");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Kode OTP telah dikirim ulang ke email Anda"),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
} else {
tts.speak(response.message.isNotEmpty
? response.message
: "Gagal mengirim ulang OTP");
}
}
@override
Widget build(BuildContext context) {
tts.speak("Halaman verifikasi kode OTP");
tts.speak("Halaman verifikasi kode OTP");
return Scaffold(
backgroundColor: AppColors.background,
@ -134,7 +215,7 @@ class OTPScreen extends StatelessWidget {
// 📌 INFO TEXT
Text(
"Kode ini hanya berlaku selama 1 menit setelah dikirim",
"Kode ini hanya berlaku selama 2 menit setelah dikirim",
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
@ -165,14 +246,7 @@ class OTPScreen extends StatelessWidget {
SizedBox(height: 8),
TextButton(
onPressed: () {
tts.speak("Kode OTP telah dikirim ulang");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Kode OTP telah dikirim ulang ke email Anda"),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
resendOtp();
},
child: Text(
"Kirim Ulang Kode",

View File

@ -2,11 +2,20 @@ import 'package:flutter/material.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart';
import '../services/tts_service.dart';
import '../services/reset_password_api_service.dart';
import '../models/reset_password_request.dart';
import '../utils/colors.dart';
import 'login_screen.dart';
class ResetPasswordScreen extends StatefulWidget {
const ResetPasswordScreen({super.key});
final String email;
final String token;
const ResetPasswordScreen({
super.key,
required this.email,
required this.token,
});
@override
State<ResetPasswordScreen> createState() => _ResetPasswordScreenState();
@ -16,6 +25,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final TextEditingController newPasswordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
final TTSService tts = TTSService();
final ResetPasswordApiService resetApi = ResetPasswordApiService();
String passwordStrength = ""; // Track password strength
@override
@ -91,7 +101,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
}
// ============ HANDLE RESET PASSWORD ============
void resetPassword(BuildContext context) {
Future<void> resetPassword(BuildContext context) async {
String newPass = newPasswordController.text.trim();
String confirmPass = confirmPasswordController.text.trim();
@ -113,13 +123,32 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
return;
}
tts.speak("Password berhasil diubah, silakan login kembali");
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => LoginScreen()),
(route) => false,
final response = await resetApi.resetPassword(
ResetPasswordRequest(
email: widget.email,
token: widget.token,
password: newPass,
),
);
if (!mounted) {
return;
}
if (response.isSuccess) {
tts.speak(response.message.isNotEmpty
? response.message
: "Password berhasil diubah, silakan login kembali");
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => LoginScreen()),
(route) => false,
);
} else {
tts.speak(response.message.isNotEmpty
? response.message
: "Token tidak valid");
}
}
@override

View File

@ -0,0 +1,16 @@
import 'dart:io';
class ApiConfig {
// Ubah ke true jika memakai emulator Android (10.0.2.2).
static const bool useAndroidEmulator = false;
// Ganti IP ini dengan IP laptop/PC Anda saat menggunakan HP fisik (ADB).
static const String deviceHost = "192.168.0.31:8000";
static String get baseUrl {
if (Platform.isAndroid && useAndroidEmulator) {
return "http://10.0.2.2:8000";
}
return "http://$deviceHost";
}
}

View File

@ -0,0 +1,31 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/forgot_password_request.dart';
import '../models/forgot_password_response.dart';
import 'api_config.dart';
class ForgotPasswordApiService {
Future<ForgotPasswordResponse> requestReset(
ForgotPasswordRequest request,
) async {
final Uri url = Uri.parse("${ApiConfig.baseUrl}/forgot-password");
try {
final http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(request.toJson()),
);
final Map<String, dynamic> jsonBody =
jsonDecode(response.body) as Map<String, dynamic>;
return ForgotPasswordResponse.fromJson(jsonBody);
} catch (error) {
return const ForgotPasswordResponse(
status: "error",
message: "Gagal terhubung ke server",
);
}
}
}

View File

@ -1,27 +1,15 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/register_request.dart';
import '../models/register_response.dart';
import 'api_config.dart';
class RegisterApiService {
// Ubah nilai ini jika ingin memakai emulator Android (10.0.2.2).
static const bool _useAndroidEmulator = false;
// Ganti IP ini dengan IP laptop/PC Anda saat menggunakan HP fisik (ADB).
static const String _deviceHost = "192.168.18.14:8000";
static String get _baseUrl {
if (Platform.isAndroid && _useAndroidEmulator) {
return "http://10.0.2.2:8000";
}
return "http://$_deviceHost";
}
static const String _tokenKey = "auth_token";
Future<RegisterResponse> register(RegisterRequest request) async {
final Uri url = Uri.parse("$_baseUrl/register");
final Uri url = Uri.parse("${ApiConfig.baseUrl}/register");
try {
final http.Response response = await http.post(

View File

@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/reset_password_request.dart';
import '../models/reset_password_response.dart';
import 'api_config.dart';
class ResetPasswordApiService {
Future<ResetPasswordResponse> resetPassword(
ResetPasswordRequest request,
) async {
final Uri url = Uri.parse("${ApiConfig.baseUrl}/reset-password");
try {
final http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(request.toJson()),
);
if (response.body.isEmpty) {
return ResetPasswordResponse(
status: "error",
message:
"Respons server kosong (status ${response.statusCode})",
);
}
Map<String, dynamic> jsonBody;
try {
jsonBody = jsonDecode(response.body) as Map<String, dynamic>;
} catch (_) {
return ResetPasswordResponse(
status: "error",
message:
"Respons server tidak valid (status ${response.statusCode})",
);
}
final ResetPasswordResponse parsed =
ResetPasswordResponse.fromJson(jsonBody);
if (response.statusCode >= 400 && parsed.message.isEmpty) {
return ResetPasswordResponse(
status: "error",
message:
"Gagal reset password (status ${response.statusCode})",
);
}
return parsed;
} catch (error) {
return const ResetPasswordResponse(
status: "error",
message: "Gagal terhubung ke server",
);
}
}
}