connecting API to forgot password, otp screen and reset password, along with making API files into 1 single file
This commit is contained in:
parent
62bc1cb0e2
commit
650e17012f
|
|
@ -0,0 +1,9 @@
|
|||
class ForgotPasswordRequest {
|
||||
final String email;
|
||||
|
||||
const ForgotPasswordRequest({required this.email});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {"email": email};
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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");
|
||||
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()),
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OTPScreen(
|
||||
email: email,
|
||||
serverToken: response.token!,
|
||||
expiredAt: response.expired!,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
tts.speak(response.message.isNotEmpty
|
||||
? response.message
|
||||
: "Email tidak ditemukan");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -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,15 +54,70 @@ 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");
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue