Offers And Discounts Apis

offers_discounts
Aamir Muhammad 1 month ago
parent 369a566006
commit b02831834f

@ -302,9 +302,12 @@ class ApiConsts {
// POMS Offers and Discounts API
static final pomsBaseUrl = 'https://poms-uat.odoo.com';
static final pomsAuthPatient = '$pomsBaseUrl/api/v1/mobile/auth/patient';
static final pomsGetHospitalOffers = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetHospitalOffersDetails = '$pomsBaseUrl/api/v1/mobile/hospital-offers';
static final pomsGetOffersAndDiscountPurchasedHistory = '$pomsBaseUrl/api/v1/mobile/my/offers';
static String pomsPurchaseOffer(int offerId) => '$pomsBaseUrl/api/v1/mobile/offers/$offerId/purchase';
static String pomsPaymentResult(int orderId) => '$pomsBaseUrl/api/v1/mobile/orders/$orderId/payment-result';
// ************ static values for Api ****************
static final double appVersionID = 21.3;

@ -56,7 +56,7 @@ class AppState {
void setIsPayfortHostedPageEnabled(bool value) {
isPayfortHostedPageEnabled = value;
// isPayfortHostedPageEnabled = true;
// isPayfortHostedPageEnabled = false;
}
int? _superUserID;
@ -170,6 +170,31 @@ class AppState {
set setIsAuthenticated(v) => isAuthenticated = v;
// POMS Authentication for Offers and Discounts
String _pomsAuthToken = "";
DateTime? _pomsAuthTokenExpiresAt;
String get getPomsAuthToken => _pomsAuthToken;
DateTime? get getPomsAuthTokenExpiresAt => _pomsAuthTokenExpiresAt;
void setPomsAuthToken(String token, DateTime expiresAt) {
_pomsAuthToken = token;
_pomsAuthTokenExpiresAt = expiresAt;
}
void clearPomsAuthToken() {
_pomsAuthToken = "";
_pomsAuthTokenExpiresAt = null;
}
bool isPomsAuthTokenValid() {
if (_pomsAuthToken.isEmpty || _pomsAuthTokenExpiresAt == null) {
return false;
}
return DateTime.now().isBefore(_pomsAuthTokenExpiresAt!);
}
String deviceTypeID = "";

@ -336,7 +336,7 @@ extension ServiceTypeEnumExt on ServiceTypeEnum {
case ServiceTypeEnum.liveCareAppointment:
return 4;
case ServiceTypeEnum.offerPayment:
return 5;
return 18;
}
}
}

@ -0,0 +1,30 @@
import 'dart:convert';
class OfferPurchaseRequestModel {
String identificationType;
String idValue;
String mobileNumber;
OfferPurchaseRequestModel({
required this.identificationType,
required this.idValue,
required this.mobileNumber,
});
factory OfferPurchaseRequestModel.fromRawJson(String str) => OfferPurchaseRequestModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseRequestModel.fromJson(Map<String, dynamic> json) => OfferPurchaseRequestModel(
identificationType: json["identification_type"],
idValue: json["id_value"],
mobileNumber: json["mobile_number"],
);
Map<String, dynamic> toJson() => {
"identification_type": identificationType,
"id_value": idValue,
"mobile_number": mobileNumber,
};
}

@ -0,0 +1,41 @@
import 'dart:convert';
class OfferPurchaseResponseModel {
int orderId;
String orderRef;
String amount;
String currency;
bool paymentRequired;
dynamic paymentUrl;
OfferPurchaseResponseModel({
required this.orderId,
required this.orderRef,
required this.amount,
required this.currency,
required this.paymentRequired,
required this.paymentUrl,
});
factory OfferPurchaseResponseModel.fromRawJson(String str) => OfferPurchaseResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseResponseModel.fromJson(Map<String, dynamic> json) => OfferPurchaseResponseModel(
orderId: json["order_id"],
orderRef: json["order_ref"],
amount: json["amount"],
currency: json["currency"],
paymentRequired: json["payment_required"],
paymentUrl: json["payment_url"],
);
Map<String, dynamic> toJson() => {
"order_id": orderId,
"order_ref": orderRef,
"amount": amount,
"currency": currency,
"payment_required": paymentRequired,
"payment_url": paymentUrl,
};
}

@ -0,0 +1,229 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
List<Datum> data;
Meta meta;
OfferPurchaseHistoryResponseModel({
required this.data,
required this.meta,
});
factory OfferPurchaseHistoryResponseModel.fromRawJson(String str) => OfferPurchaseHistoryResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseHistoryResponseModel.fromJson(Map<String, dynamic> json) => OfferPurchaseHistoryResponseModel(
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
meta: Meta.fromJson(json["meta"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
};
}
class Datum {
String voucherCode;
String state;
DateTime purchaseDate;
dynamic redemptionDate;
String qrCode;
String qrCodeUrl;
String pdfUrl;
DateTime validTo;
UsageValidity usageValidity;
Offer offer;
String branchApplicability;
List<Branch> branches;
Datum({
required this.voucherCode,
required this.state,
required this.purchaseDate,
required this.redemptionDate,
required this.qrCode,
required this.qrCodeUrl,
required this.pdfUrl,
required this.validTo,
required this.usageValidity,
required this.offer,
required this.branchApplicability,
required this.branches,
});
factory Datum.fromRawJson(String str) => Datum.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
voucherCode: json["voucher_code"],
state: json["state"],
purchaseDate: DateTime.parse(json["purchase_date"]),
redemptionDate: json["redemption_date"],
qrCode: json["qr_code"],
qrCodeUrl: json["qr_code_url"],
pdfUrl: json["pdf_url"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
offer: Offer.fromJson(json["offer"]),
branchApplicability: json["branch_applicability"],
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"voucher_code": voucherCode,
"state": state,
"purchase_date": purchaseDate.toIso8601String(),
"redemption_date": redemptionDate,
"qr_code": qrCode,
"qr_code_url": qrCodeUrl,
"pdf_url": pdfUrl,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
"offer": offer.toJson(),
"branch_applicability": branchApplicability,
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
};
}
class Branch {
String code;
String name;
String nameAr;
Branch({
required this.code,
required this.name,
required this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Branch.fromJson(Map<String, dynamic> json) => Branch(
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Offer {
int id;
String name;
String nameAr;
String branchApplicability;
DateTime validTo;
UsageValidity usageValidity;
Offer({
required this.id,
required this.name,
required this.nameAr,
required this.branchApplicability,
required this.validTo,
required this.usageValidity,
});
factory Offer.fromRawJson(String str) => Offer.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Offer.fromJson(Map<String, dynamic> json) => Offer(
id: json["id"],
name: json["name"],
nameAr: json["name_ar"],
branchApplicability: json["branch_applicability"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"name_ar": nameAr,
"branch_applicability": branchApplicability,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
};
}
class UsageValidity {
String type;
dynamic days;
String rule;
String ruleAr;
DateTime redeemBy;
String redeemByDisplay;
UsageValidity({
required this.type,
required this.days,
required this.rule,
required this.ruleAr,
required this.redeemBy,
required this.redeemByDisplay,
});
factory UsageValidity.fromRawJson(String str) => UsageValidity.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory UsageValidity.fromJson(Map<String, dynamic> json) => UsageValidity(
type: json["type"],
days: json["days"],
rule: json["rule"],
ruleAr: json["rule_ar"],
redeemBy: DateTime.parse(json["redeem_by"]),
redeemByDisplay: json["redeem_by_display"],
);
Map<String, dynamic> toJson() => {
"type": type,
"days": days,
"rule": rule,
"rule_ar": ruleAr,
"redeem_by": "${redeemBy.year.toString().padLeft(4, '0')}-${redeemBy.month.toString().padLeft(2, '0')}-${redeemBy.day.toString().padLeft(2, '0')}",
"redeem_by_display": redeemByDisplay,
};
}
class Meta {
int total;
int page;
int perPage;
int pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
total: json["total"],
page: json["page"],
perPage: json["per_page"],
pages: json["pages"],
);
Map<String, dynamic> toJson() => {
"total": total,
"page": page,
"per_page": perPage,
"pages": pages,
};
}

@ -0,0 +1,229 @@
import 'dart:convert';
class OfferPurchaseHistoryResponseModel {
List<Datum> data;
Meta meta;
OfferPurchaseHistoryResponseModel({
required this.data,
required this.meta,
});
factory OfferPurchaseHistoryResponseModel.fromRawJson(String str) => OfferPurchaseHistoryResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory OfferPurchaseHistoryResponseModel.fromJson(Map<String, dynamic> json) => OfferPurchaseHistoryResponseModel(
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
meta: Meta.fromJson(json["meta"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"meta": meta.toJson(),
};
}
class Datum {
String voucherCode;
String state;
DateTime purchaseDate;
dynamic redemptionDate;
String qrCode;
String qrCodeUrl;
String pdfUrl;
DateTime validTo;
UsageValidity usageValidity;
Offer offer;
String branchApplicability;
List<Branch> branches;
Datum({
required this.voucherCode,
required this.state,
required this.purchaseDate,
required this.redemptionDate,
required this.qrCode,
required this.qrCodeUrl,
required this.pdfUrl,
required this.validTo,
required this.usageValidity,
required this.offer,
required this.branchApplicability,
required this.branches,
});
factory Datum.fromRawJson(String str) => Datum.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
voucherCode: json["voucher_code"],
state: json["state"],
purchaseDate: DateTime.parse(json["purchase_date"]),
redemptionDate: json["redemption_date"],
qrCode: json["qr_code"],
qrCodeUrl: json["qr_code_url"],
pdfUrl: json["pdf_url"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
offer: Offer.fromJson(json["offer"]),
branchApplicability: json["branch_applicability"],
branches: List<Branch>.from(json["branches"].map((x) => Branch.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"voucher_code": voucherCode,
"state": state,
"purchase_date": purchaseDate.toIso8601String(),
"redemption_date": redemptionDate,
"qr_code": qrCode,
"qr_code_url": qrCodeUrl,
"pdf_url": pdfUrl,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
"offer": offer.toJson(),
"branch_applicability": branchApplicability,
"branches": List<dynamic>.from(branches.map((x) => x.toJson())),
};
}
class Branch {
String code;
String name;
String nameAr;
Branch({
required this.code,
required this.name,
required this.nameAr,
});
factory Branch.fromRawJson(String str) => Branch.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Branch.fromJson(Map<String, dynamic> json) => Branch(
code: json["code"],
name: json["name"],
nameAr: json["name_ar"],
);
Map<String, dynamic> toJson() => {
"code": code,
"name": name,
"name_ar": nameAr,
};
}
class Offer {
int id;
String name;
String nameAr;
String branchApplicability;
DateTime validTo;
UsageValidity usageValidity;
Offer({
required this.id,
required this.name,
required this.nameAr,
required this.branchApplicability,
required this.validTo,
required this.usageValidity,
});
factory Offer.fromRawJson(String str) => Offer.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Offer.fromJson(Map<String, dynamic> json) => Offer(
id: json["id"],
name: json["name"],
nameAr: json["name_ar"],
branchApplicability: json["branch_applicability"],
validTo: DateTime.parse(json["valid_to"]),
usageValidity: UsageValidity.fromJson(json["usage_validity"]),
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"name_ar": nameAr,
"branch_applicability": branchApplicability,
"valid_to": "${validTo.year.toString().padLeft(4, '0')}-${validTo.month.toString().padLeft(2, '0')}-${validTo.day.toString().padLeft(2, '0')}",
"usage_validity": usageValidity.toJson(),
};
}
class UsageValidity {
String type;
dynamic days;
String rule;
String ruleAr;
DateTime redeemBy;
String redeemByDisplay;
UsageValidity({
required this.type,
required this.days,
required this.rule,
required this.ruleAr,
required this.redeemBy,
required this.redeemByDisplay,
});
factory UsageValidity.fromRawJson(String str) => UsageValidity.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory UsageValidity.fromJson(Map<String, dynamic> json) => UsageValidity(
type: json["type"],
days: json["days"],
rule: json["rule"],
ruleAr: json["rule_ar"],
redeemBy: DateTime.parse(json["redeem_by"]),
redeemByDisplay: json["redeem_by_display"],
);
Map<String, dynamic> toJson() => {
"type": type,
"days": days,
"rule": rule,
"rule_ar": ruleAr,
"redeem_by": "${redeemBy.year.toString().padLeft(4, '0')}-${redeemBy.month.toString().padLeft(2, '0')}-${redeemBy.day.toString().padLeft(2, '0')}",
"redeem_by_display": redeemByDisplay,
};
}
class Meta {
int total;
int page;
int perPage;
int pages;
Meta({
required this.total,
required this.page,
required this.perPage,
required this.pages,
});
factory Meta.fromRawJson(String str) => Meta.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
total: json["total"],
page: json["page"],
perPage: json["per_page"],
pages: json["pages"],
);
Map<String, dynamic> toJson() => {
"total": total,
"page": page,
"per_page": perPage,
"pages": pages,
};
}

@ -0,0 +1,34 @@
import 'dart:convert';
class PaymentResultRequestModel {
String status;
String paymentRef;
String amount;
String currency;
PaymentResultRequestModel({
required this.status,
required this.paymentRef,
required this.amount,
required this.currency,
});
factory PaymentResultRequestModel.fromRawJson(String str) => PaymentResultRequestModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PaymentResultRequestModel.fromJson(Map<String, dynamic> json) => PaymentResultRequestModel(
status: json["status"],
paymentRef: json["payment_ref"],
amount: json["amount"],
currency: json["currency"],
);
Map<String, dynamic> toJson() => {
"status": status,
"payment_ref": paymentRef,
"amount": amount,
"currency": currency,
};
}

@ -0,0 +1,22 @@
import 'dart:convert';
class PaymentResultResponseModel {
String status;
PaymentResultResponseModel({
required this.status,
});
factory PaymentResultResponseModel.fromRawJson(String str) => PaymentResultResponseModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PaymentResultResponseModel.fromJson(Map<String, dynamic> json) => PaymentResultResponseModel(
status: json["status"],
);
Map<String, dynamic> toJson() => {
"status": status,
};
}

@ -0,0 +1,92 @@
import 'dart:convert';
class PomsAuthTokenModel {
String token;
DateTime expiresAt;
Partner partner;
PortalAccount portalAccount;
PomsAuthTokenModel({
required this.token,
required this.expiresAt,
required this.partner,
required this.portalAccount,
});
factory PomsAuthTokenModel.fromRawJson(String str) => PomsAuthTokenModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PomsAuthTokenModel.fromJson(Map<String, dynamic> json) => PomsAuthTokenModel(
token: json["token"],
expiresAt: DateTime.parse(json["expires_at"]),
partner: Partner.fromJson(json["partner"]),
portalAccount: PortalAccount.fromJson(json["portal_account"]),
);
Map<String, dynamic> toJson() => {
"token": token,
"expires_at": expiresAt.toIso8601String(),
"partner": partner.toJson(),
"portal_account": portalAccount.toJson(),
};
// Helper method to check if token is expired
bool isExpired() {
return DateTime.now().isAfter(expiresAt);
}
// Helper method to check if token is valid
bool isValid() {
return !isExpired() && token.isNotEmpty;
}
}
class Partner {
int id;
String name;
Partner({
required this.id,
required this.name,
});
factory Partner.fromRawJson(String str) => Partner.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Partner.fromJson(Map<String, dynamic> json) => Partner(
id: json["id"],
name: json["name"],
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
};
}
class PortalAccount {
bool created;
String login;
PortalAccount({
required this.created,
required this.login,
});
factory PortalAccount.fromRawJson(String str) => PortalAccount.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PortalAccount.fromJson(Map<String, dynamic> json) => PortalAccount(
created: json["created"],
login: json["login"],
);
Map<String, dynamic> toJson() => {
"created": created,
"login": login,
};
}

@ -5,14 +5,26 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/poms_auth_token_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_purchases_history_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class OffersAndDiscountsRepo {
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String patientId, String mobileNumber);
Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts();
Future<Either<Failure, GenericApiModel<OffersAndDiscountsDetailedResponseModel>>> getOfferDetails(int offerId);
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory();
Future<Either<Failure, GenericApiModel<OfferPurchaseHistoryResponseModel>>> getOffersAndDiscountsHistory(String authToken);
Future<Either<Failure, GenericApiModel<OfferPurchaseResponseModel>>> purchaseOffer(int offerId, OfferPurchaseRequestModel request, String authToken);
Future<Either<Failure, GenericApiModel<PaymentResultResponseModel>>> sendPaymentResult(int orderId, PaymentResultRequestModel request, String authToken);
}
class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
@ -21,6 +33,57 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<PomsAuthTokenModel>>> authenticatePatient(String patientId, String mobileNumber) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
};
Map<String, dynamic> body = {
'patient_id': patientId,
'mobile_number': mobileNumber,
};
try {
GenericApiModel<PomsAuthTokenModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.pomsAuthPatient,
body: body,
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final authResponse = PomsAuthTokenModel.fromJson(response);
apiResponse = GenericApiModel<PomsAuthTokenModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: authResponse,
);
} catch (e) {
loggerService.logError("Error parsing POMS auth response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in authenticatePatient: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<OffersAndDiscountsResponseModel>>> getOffersAndDiscounts() async {
Map<String, String> headers = {
@ -109,42 +172,142 @@ class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
}
@override
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory() async {
Map<String, dynamic> mapDevice = {};
@override
Future<Either<Failure, GenericApiModel<OfferPurchaseHistoryResponseModel>>> getOffersAndDiscountsHistory(String authToken) async {
Map<String, String> headers = {
'Content-Type': 'application/json',
'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey,
'Authorization': 'Bearer $authToken',
};
try {
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
GenericApiModel<OfferPurchaseHistoryResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
await apiClient.get(
ApiConsts.pomsGetOffersAndDiscountPurchasedHistory,
body: mapDevice,
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final historyResponse = OfferPurchaseHistoryResponseModel.fromJson(response);
apiResponse = GenericApiModel<OfferPurchaseHistoryResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: historyResponse,
);
} catch (e) {
loggerService.logError("Error parsing offer purchase history response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in getOffersAndDiscountsHistory: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<OfferPurchaseResponseModel>>> purchaseOffer(
int offerId,
OfferPurchaseRequestModel request,
String authToken,
) async {
Map<String, String> headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey, 'Authorization': 'Bearer $authToken'};
try {
GenericApiModel<OfferPurchaseResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.pomsPurchaseOffer(offerId),
body: request.toJson(),
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['List_OffersAndDiscountsHistory'];
if (list == null || list.isEmpty) {
throw Exception("offers and discounts history list is empty");
}
final purchaseResponse = OfferPurchaseResponseModel.fromJson(response);
apiResponse = GenericApiModel<OfferPurchaseResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: purchaseResponse,
);
} catch (e) {
loggerService.logError("Error parsing offer purchase response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in purchaseOffer: $e");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<PaymentResultResponseModel>>> sendPaymentResult(
int orderId,
PaymentResultRequestModel request,
String authToken,
) async {
Map<String, String> headers = {'Content-Type': 'application/json', 'X-POMS-CHANNEL-API-KEY': ApiConsts.pomsApiKey, 'Authorization': 'Bearer $authToken'};
final historyOffers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>();
try {
GenericApiModel<PaymentResultResponseModel>? apiResponse;
Failure? failure;
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>(
messageStatus: messageStatus,
await apiClient.post(
ApiConsts.pomsPaymentResult(orderId),
body: request.toJson(),
apiHeaders: headers,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final paymentResultResponse = PaymentResultResponseModel.fromJson(response);
apiResponse = GenericApiModel<PaymentResultResponseModel>(
messageStatus: 1,
statusCode: statusCode,
errorMessage: null,
data: historyOffers,
data: paymentResultResponse,
);
} catch (e) {
loggerService.logError("Error parsing payment result response: $e");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
loggerService.logError("Exception in sendPaymentResult: $e");
return Left(UnknownFailure(e.toString()));
}
}

@ -1,6 +1,14 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart' hide Category;
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/poms_auth_token_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offer_purchase_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_request_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/payment_result_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_purchases_history_response_model.dart' as history_model;
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -8,6 +16,8 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
bool isOffersLoading = false;
bool isHistoryLoading = false;
bool isOfferDetailsLoading = false;
bool isAuthenticating = false;
bool isPurchasing = false;
List<int> selectedCategories = []; // Empty means 'All Offers'
String searchQuery = '';
@ -15,10 +25,12 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
ErrorHandlerService errorHandlerService;
List<OfferData> offersList = [];
List<OffersAndDiscountsResponseModel> historyList = [];
history_model.OfferPurchaseHistoryResponseModel? purchaseHistory;
OffersAndDiscountsDetailedResponseModel? offerDetails;
OffersAndDiscountsDetailedResponseModel? selectedOffer;
Meta? meta;
PomsAuthTokenModel? pomsAuthToken;
OfferPurchaseResponseModel? lastPurchaseResponse;
OffersAndDiscountsViewModel({
required this.offersAndDiscountsRepo,
@ -28,11 +40,27 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
// For loading state compatibility with existing code
bool get isRegionListLoading => isOffersLoading;
initOffersAndDiscounts() {
initOffersAndDiscounts({bool forceRefresh = false}) {
// Only clear data if force refresh, otherwise keep existing data
if (forceRefresh) {
offersList.clear();
}
// Only show loading if we don't have data yet
if (offersList.isEmpty) {
isOffersLoading = true;
notifyListeners();
}
getOffersAndDiscounts();
}
// Method to force refresh the offers list
Future<void> refreshOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
offersList.clear();
isOffersLoading = true;
notifyListeners();
getOffersAndDiscounts();
await getOffersAndDiscounts(onSuccess: onSuccess, onError: onError);
}
setIsOffersLoading(bool val) {
@ -45,6 +73,204 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
notifyListeners();
}
setIsAuthenticating(bool val) {
isAuthenticating = val;
notifyListeners();
}
/// Authenticate patient before making a purchase
Future<bool> authenticatePatient({Function(PomsAuthTokenModel)? onSuccess, Function(String)? onError}) async {
final appState = getIt.get<AppState>();
final authenticatedUser = appState.getAuthenticatedUser();
if (authenticatedUser == null) {
if (onError != null) {
onError("User not authenticated");
}
return false;
}
// Check if token is already valid
if (appState.isPomsAuthTokenValid()) {
if (onSuccess != null && pomsAuthToken != null) {
onSuccess(pomsAuthToken!);
}
return true;
}
isAuthenticating = true;
notifyListeners();
final result = await offersAndDiscountsRepo.authenticatePatient(
authenticatedUser.patientId.toString(),
// "4774103",
// "2001273",
authenticatedUser.mobileNumber ?? "",
);
result.fold(
(failure) async {
isAuthenticating = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isAuthenticating = false;
if (response.data != null) {
pomsAuthToken = response.data;
// Store token in AppState
appState.setPomsAuthToken(response.data!.token, response.data!.expiresAt);
}
notifyListeners();
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return pomsAuthToken != null && pomsAuthToken!.isValid();
}
/// Ensure patient is authenticated, re-authenticate if token is invalid or expired
/// This is a convenience method for Buy Now buttons and other actions requiring auth
Future<bool> ensureAuthenticated({
Function()? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Check if token is already valid
if (appState.isPomsAuthTokenValid()) {
if (onSuccess != null) {
onSuccess();
}
return true;
}
// Token is invalid or expired, re-authenticate
final isAuthenticated = await authenticatePatient(
onSuccess: (authToken) {
print("POMS Authentication successful: ${authToken.token}");
if (onSuccess != null) {
onSuccess();
}
},
onError: (error) {
print("POMS Authentication failed: $error");
if (onError != null) {
onError(error);
}
},
);
return isAuthenticated;
}
/// Purchase an offer after payment is successful
Future<bool> purchaseOffer({
required int offerId,
required String identificationType,
required String idValue,
required String mobileNumber,
Function(OfferPurchaseResponseModel)? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return false;
}
isPurchasing = true;
notifyListeners();
final request = OfferPurchaseRequestModel(identificationType: identificationType, idValue: idValue, mobileNumber: mobileNumber);
final result = await offersAndDiscountsRepo.purchaseOffer(offerId, request, appState.getPomsAuthToken);
result.fold(
(failure) async {
isPurchasing = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isPurchasing = false;
if (response.data != null) {
lastPurchaseResponse = response.data;
print(lastPurchaseResponse!.toRawJson());
}
notifyListeners();
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return lastPurchaseResponse != null;
}
/// Send payment result to backend after successful payment
Future<bool> sendPaymentResult({
required int orderId,
required String status,
required String paymentRef,
required String amount,
required String currency,
Function(PaymentResultResponseModel)? onSuccess,
Function(String)? onError,
}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return false;
}
final request = PaymentResultRequestModel(
status: status,
paymentRef: paymentRef,
amount: amount,
currency: currency,
);
final result = await offersAndDiscountsRepo.sendPaymentResult(orderId, request, appState.getPomsAuthToken);
bool success = false;
result.fold(
(failure) async {
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
if (response.data != null) {
success = response.data!.status.toLowerCase() == "ok";
}
if (onSuccess != null && response.data != null) {
onSuccess(response.data!);
}
},
);
return success;
}
setSelectedCategories(List<int> categoryIds) {
selectedCategories = categoryIds;
notifyListeners();
@ -75,8 +301,13 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
isOffersLoading = true;
notifyListeners();
// Only show loading if we don't have data yet
final bool hasExistingData = offersList.isNotEmpty;
if (!hasExistingData) {
isOffersLoading = true;
notifyListeners();
}
final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
@ -84,7 +315,7 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
(failure) async {
isOffersLoading = false;
notifyListeners();
// Keep dummy data if API fails - don't clear the list
// Keep existing data if API fails
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
@ -93,10 +324,22 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
(response) {
isOffersLoading = false;
if (response.data != null) {
offersList = response.data!.data;
// If we already have data, append new data instead of replacing
if (hasExistingData) {
// Get new items that don't already exist in the list
final existingIds = offersList.map((e) => e.id).toSet();
final newItems = response.data!.data.where((item) => !existingIds.contains(item.id)).toList();
// Append only new items
if (newItems.isNotEmpty) {
offersList.addAll(newItems);
}
} else {
// First load - replace the list
offersList = response.data!.data;
}
meta = response.data!.meta;
}
// If API returns empty or null, keep the dummy data
notifyListeners();
if (onSuccess != null) {
onSuccess(response);
@ -134,27 +377,37 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
}
Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final appState = getIt.get<AppState>();
// Ensure we have a valid auth token
if (!appState.isPomsAuthTokenValid()) {
isHistoryLoading = false;
notifyListeners();
if (onError != null) {
onError("Authentication token is invalid or expired");
}
return;
}
isHistoryLoading = true;
notifyListeners();
final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory();
final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory(appState.getPomsAuthToken);
result.fold(
(failure) async {
isHistoryLoading = false;
notifyListeners();
// Keep dummy data if API fails - don't clear the list
// await errorHandlerService.handleError(failure: failure);
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(response) {
isHistoryLoading = false;
if (response.data != null && response.data!.isNotEmpty) {
historyList = response.data!;
if (response.data != null) {
purchaseHistory = response.data;
}
// If API returns empty or null, keep the dummy data
notifyListeners();
if (onSuccess != null) {
onSuccess(response);
@ -163,6 +416,10 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
);
}
List<history_model.Datum> get historyList {
return purchaseHistory?.data ?? [];
}
List<OfferData> get filteredOffers {
List<OfferData> filtered = offersList;
@ -185,14 +442,16 @@ class OffersAndDiscountsViewModel extends ChangeNotifier {
return filtered;
}
List<OffersAndDiscountsResponseModel> get filteredHistory {
List<OffersAndDiscountsResponseModel> filtered = historyList;
List<history_model.Datum> get filteredHistory {
List<history_model.Datum> filtered = historyList;
// Filter by search query
if (searchQuery.isNotEmpty) {
filtered = filtered.where((offer) {
// Assuming history list uses the old structure - update when API is available
return true; // Placeholder for now
filtered = filtered.where((history) {
final offerNameMatch = history.offer.name.toLowerCase().contains(searchQuery.toLowerCase());
final offerNameArMatch = history.offer.nameAr.toLowerCase().contains(searchQuery.toLowerCase());
final voucherCodeMatch = history.voucherCode.toLowerCase().contains(searchQuery.toLowerCase());
return offerNameMatch || offerNameArMatch || voucherCodeMatch;
}).toList();
}

@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
@ -99,6 +100,7 @@ class _LandingPageState extends State<LandingPage> {
late EmergencyServicesViewModel emergencyServicesViewModel;
late TodoSectionViewModel todoSectionViewModel;
late NotificationsViewModel notificationsViewModel;
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
final SwiperController _controller = SwiperController();
@ -159,6 +161,7 @@ class _LandingPageState extends State<LandingPage> {
// myAppointmentsViewModel.getPatientAppointmentQueueDetails();
notificationsViewModel.initNotificationsViewModel();
insuranceViewModel.initInsuranceProvider();
offersAndDiscountsViewModel.initOffersAndDiscounts();
if (!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment(
@ -197,6 +200,7 @@ class _LandingPageState extends State<LandingPage> {
emergencyServicesViewModel = Provider.of<EmergencyServicesViewModel>(context, listen: false);
todoSectionViewModel = Provider.of<TodoSectionViewModel>(context, listen: false);
notificationsViewModel = Provider.of<NotificationsViewModel>(context, listen: false);
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
appState = getIt.get<AppState>();
return PopScope(
canPop: false,
@ -244,7 +248,6 @@ class _LandingPageState extends State<LandingPage> {
// }
// },
// );
}
},
child: SingleChildScrollView(
@ -448,7 +451,8 @@ class _LandingPageState extends State<LandingPage> {
: SizedBox.shrink(),
// Offers And Discounts Carousel - Auto-scrolling from right to left
appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel
//appState.isAuthenticated &&
appState.isEnabledOffersAndDiscountsCarousel && offersAndDiscountsViewModel.offersList.isNotEmpty
? Column(
children: [
SizedBox(height: 12.h),
@ -536,9 +540,7 @@ class _LandingPageState extends State<LandingPage> {
height: isTablet ? 290.h : 255.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3
? myAppointmentsVM.patientAppointmentsHistoryList.length
: 3,
itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3,
shrinkWrap: true,
padding: EdgeInsets.only(left: 16.h, right: 16.h),
itemBuilder: (context, index) {

@ -8,12 +8,15 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_detailed_response_model.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discount_payment_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:easy_localization/easy_localization.dart';
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
final int offerId;
@ -318,7 +321,28 @@ class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscount
text: 'Buy Now',
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
onPressed: () async {
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await vm.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page
if (vm.offerDetails != null) {
vm.setSelectedOffer(vm.offerDetails!);

@ -0,0 +1,290 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_purchases_history_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
class OffersAndDiscountsSuccessQrPage extends StatefulWidget {
final Datum purchaseData;
const OffersAndDiscountsSuccessQrPage({super.key, required this.purchaseData});
@override
State<OffersAndDiscountsSuccessQrPage> createState() => _OffersAndDiscountsSuccessQrPageState();
}
class _OffersAndDiscountsSuccessQrPageState extends State<OffersAndDiscountsSuccessQrPage> {
late AppState appState;
String _formatDate(DateTime date) {
try {
return DateFormat('dd MMM yyyy, hh:mm a').format(date);
} catch (e) {
return date.toString();
}
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return AppColors.successColor;
case 'redeemed':
case 'used':
return AppColors.warningColor;
case 'expired':
return AppColors.errorColor;
default:
return AppColors.greyTextColor;
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return LocaleKeys.active.tr(context: context);
case 'redeemed':
case 'used':
return "Redeemed";
case 'expired':
return LocaleKeys.expired.tr(context: context);
default:
return status.toUpperCase();
}
}
void _copyVoucherCode() {
Clipboard.setData(ClipboardData(text: widget.purchaseData.voucherCode));
Utils.showToast("Voucher code copied to clipboard");
}
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.paymentSuccessful.tr(context: context),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 24.h),
// Success Icon
Container(
width: 80.h,
height: 80.h,
decoration: BoxDecoration(
color: AppColors.successColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.check_circle,
size: 48.h,
color: AppColors.successColor,
),
),
SizedBox(height: 16.h),
LocaleKeys.paymentSuccessful.tr(context: context).toText24(isBold: true, fontWeight: FontWeight.w700),
SizedBox(height: 8.h),
"Your offer has been purchased successfully!".toText14(color: AppColors.greyTextColor),
SizedBox(height: 32.h),
// Order Reference Card (QR Code placeholder)
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
children: [
LocaleKeys.orderNumber.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(24.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.h),
border: Border.all(color: AppColors.greyColor.withValues(alpha: 0.3)),
),
child: Column(
children: [
// QR Code Placeholder (can be replaced with actual QR code widget later)
Container(
width: 200.h,
height: 200.h,
decoration: BoxDecoration(
color: AppColors.bgScaffoldColor,
borderRadius: BorderRadius.circular(8.h),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code,
size: 80.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
"QR Code".toText12(color: AppColors.greyTextColor),
],
),
),
),
SizedBox(height: 16.h),
widget.purchaseData.voucherCode.toText20(isBold: true, weight: FontWeight.w700),
SizedBox(height: 8.h),
GestureDetector(
onTap: _copyVoucherCode,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.copy, size: 16.h, color: AppColors.primaryRedColor),
SizedBox(width: 4.w),
"Copy".toText12(color: AppColors.primaryRedColor),
],
),
),
],
),
),
SizedBox(height: 12.h),
"Show this at the facility to redeem your offer".toText12(color: AppColors.greyTextColor),
],
).paddingSymmetrical(24.h, 24.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
// Order Details Card
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.orderDetails.tr(context: context).toText18(isBold: true, weight: FontWeight.w700),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.h, vertical: 6.h),
decoration: BoxDecoration(
color: _getStatusColor(widget.purchaseData.state).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.h),
),
child: _getStatusText(widget.purchaseData.state).toText12(
isBold: true,
color: _getStatusColor(widget.purchaseData.state),
),
),
],
),
SizedBox(height: 20.h),
// Purchase Date
_buildDetailRow(
LocaleKeys.date.tr(context: context),
_formatDate(widget.purchaseData.purchaseDate),
),
SizedBox(height: 12.h),
// Valid Until
_buildDetailRow(
"Valid Until",
_formatDate(widget.purchaseData.validTo),
),
SizedBox(height: 12.h),
// Offer Name
_buildDetailRow(
"Offer Name",
appState.getLanguageID() == 1 ? widget.purchaseData.offer.nameAr : widget.purchaseData.offer.name,
),
SizedBox(height: 12.h),
Divider(color: AppColors.greyColor.withValues(alpha: 0.3)),
SizedBox(height: 12.h),
// Show offer description if available (note: Datum's Offer doesn't have description)
if (widget.purchaseData.usageValidity.rule.isNotEmpty) ...[
LocaleKeys.description.tr(context: context).toText16(isBold: true, weight: FontWeight.w700),
SizedBox(height: 8.h),
(appState.getLanguageID() == 1 ? widget.purchaseData.usageValidity.ruleAr : widget.purchaseData.usageValidity.rule).toText14(color: AppColors.greyTextColor),
],
],
).paddingSymmetrical(24.h, 24.h),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 32.h),
// Back to Home Button
CustomButton(
text: LocaleKeys.done.tr(context: context),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
height: 56.h,
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
],
),
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: label.toText14(color: AppColors.greyTextColor),
),
SizedBox(width: 16.w),
Expanded(
flex: 3,
child: Align(
alignment: Alignment.centerRight,
child: value.toText14(isBold: true),
),
),
],
);
}
}

@ -22,7 +22,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart';
import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -47,7 +47,6 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
late AppState appState;
late PayTabsViewModel paytabsViewModel;
MyInAppBrowser? browser;
String selectedPaymentMethod = "";
@ -159,24 +158,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.listPrice,
selectedOffer.vatExclusivePrice,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
@ -188,35 +189,15 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
});
} else {
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(),
amount: selectedOffer.vatExclusivePrice.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
@ -230,7 +211,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
},
);
} else {
openPaymentURL("mada", selectedOffer.listPrice);
LoaderBottomSheet.hideLoader();
openPaymentURL("mada", selectedOffer.vatExclusivePrice);
}
}
}),
@ -274,24 +256,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
// Create order first to get order_ref
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
if (appState.isPaytabsEnabled) {
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Offer Payment",
selectedOffer.listPrice,
selectedOffer.vatExclusivePrice,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
await completePaymentResult(paymentStatus: "success");
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
@ -303,35 +287,15 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
});
} else {
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: selectedOffer.listPrice.toString(),
amount: selectedOffer.vatExclusivePrice.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
await completePaymentResult(paymentStatus: "success");
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
@ -345,7 +309,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
},
);
} else {
openPaymentURL("visa", selectedOffer.listPrice);
LoaderBottomSheet.hideLoader();
openPaymentURL("visa", selectedOffer.vatExclusivePrice);
}
}
}),
@ -371,8 +336,7 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.amountBeforeTax.tr().toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.vatExclusivePrice.toString().toText16(isBold: true), AppColors.blackColor, 13,
isSaudiCurrency: selectedOffer.currency == "SAR"),
Utils.getPaymentAmountWithSymbol(selectedOffer.vatExclusivePrice.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
Row(
@ -389,24 +353,30 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17,
isSaudiCurrency: selectedOffer.currency == "SAR"),
Utils.getPaymentAmountWithSymbol(selectedOffer.listPrice.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: selectedOffer.currency == "SAR"),
],
).paddingSymmetrical(24.h, 0.h),
Platform.isIOS
? Utils.buildSvgWithAssets(
icon: AppAssets.apple_pay_button,
width: 200.h,
height: 80.h,
fit: BoxFit.contain,
applyThemeColor: false
).paddingSymmetrical(24.h, 0.h).onPress(() {
if (Utils.havePrivilege(103)) {
startApplePay(selectedOffer.listPrice);
} else {
openPaymentURL("ApplePay", selectedOffer.listPrice);
}
})
? Utils.buildSvgWithAssets(icon: AppAssets.apple_pay_button, width: 200.h, height: 80.h, fit: BoxFit.contain, applyThemeColor: false)
.paddingSymmetrical(24.h, 0.h)
.onPress(() async {
if (Utils.havePrivilege(103)) {
await startApplePay(selectedOffer.vatExclusivePrice);
} else {
// Create order first for non-privileged Apple Pay
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
LoaderBottomSheet.hideLoader();
openPaymentURL("ApplePay", selectedOffer.vatExclusivePrice);
}
})
: SizedBox(height: 12.h),
SizedBox(height: 12.h),
],
@ -470,19 +440,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (apiResponse.data["status"].toString().toLowerCase() == "success") {
tamaraOrderID = apiResponse.data["tamara_order_id"].toString();
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
await completePaymentResult(paymentStatus: "success");
} else {
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
@ -509,17 +468,8 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
onSuccess: (apiResponse) async {
debugPrint(apiResponse.data.toString());
if (payfortViewModel.payfortCheckPaymentStatusResponseModel!.responseMessage!.toLowerCase() == "success") {
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
// Order already created, just show success
await completePaymentResult(paymentStatus: "success");
} else {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
@ -540,29 +490,26 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
if (selectedOffer == null) return;
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
// Use existing transID (order_ref from createOrder)
// transID should already be set before calling this method
browser?.openPaymentBrowser(
amount,
"Offer Payment",
transID,
"12",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
selectedPaymentMethod,
appState.getAuthenticatedUser()!.patientType.toString(),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!,
browser!,
false,
"4",
selectedOffer.id.toString(),
context,
"3");
amount,
"Offer Payment",
transID,
"0",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
selectedPaymentMethod,
appState.getAuthenticatedUser()!.patientType.toString(),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!,
browser!,
false,
"18",
"0",
context,
);
}
startApplePay(double amount) async {
@ -574,18 +521,19 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
return;
}
transID = Utils.getAppointmentTransID(
selectedOffer.id,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
// Create order first to get order_ref
String? orderRef = await createOrder();
if (orderRef == null) {
// Error already shown in createOrder
return;
}
transID = orderRef;
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 2);
await payfortViewModel.getPayfortConfigurations(serviceId: ServiceTypeEnum.offerPayment.getIdFromServiceEnum(), projectId: 0, integrationId: 2);
applePayInsertRequest.clientRequestID = transID;
applePayInsertRequest.clinicID = selectedOffer.id;
@ -634,28 +582,11 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Offer Payment", amount);
paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
// TODO: Call API to complete offer purchase
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.pop(context);
paytabsViewModel.startApplePayPayment(
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
await completePaymentResult(paymentStatus: "success");
},
isFullScreen: false,
isCloseButtonVisible: true,
);
}, onError: (err) {
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
onError: (err) {});
} else {
payfortViewModel.paymentWithApplePay(
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
@ -688,4 +619,140 @@ class _OffersAndDiscountPaymentPageState extends State<OffersAndDiscountPaymentP
}
});
}
/// Create order before payment to get order reference
Future<String?> createOrder() async {
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
final authenticatedUser = appState.getAuthenticatedUser();
if (selectedOffer == null || authenticatedUser == null) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unable to create order. Missing offer or user information."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
return null;
}
// Determine identification type based on patient identification type
String identificationType = "national_id"; // Default
// if (authenticatedUser.patientIdentificationType != null) {
// switch (authenticatedUser.patientIdentificationType) {
// case 1:
// identificationType = "national_id";
// break;
// case 2:
// identificationType = "iqama";
// break;
// case 3:
// identificationType = "passport";
// break;
// case 4:
// identificationType = "gcc_id";
// break;
// default:
// identificationType = "national_id";
// }
// }
String? orderRef;
await offersAndDiscountsViewModel.purchaseOffer(
offerId: selectedOffer.id,
identificationType: identificationType,
// idValue: "2011594252",
// idValue: "4774103",
idValue: authenticatedUser.patientIdentificationNo ?? "",
mobileNumber: authenticatedUser.mobileNumber ?? "",
onSuccess: (purchaseResponse) {
// Get the order_ref from the purchase response
orderRef = purchaseResponse.orderRef;
},
onError: (error) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Failed to create order. Please try again."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
return orderRef;
}
/// Show payment success message
Future<void> completePaymentResult({required String paymentStatus}) async {
final selectedOffer = offersAndDiscountsViewModel.selectedOffer;
final lastPurchaseResponse = offersAndDiscountsViewModel.lastPurchaseResponse;
if (selectedOffer == null || lastPurchaseResponse == null) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unable to complete payment. Missing order information."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
return;
}
// Call the payment result API
await offersAndDiscountsViewModel.sendPaymentResult(
orderId: lastPurchaseResponse.orderId,
status: paymentStatus,
paymentRef: transID,
amount: selectedOffer.vatExclusivePrice.toString(),
currency: selectedOffer.currency,
onSuccess: (response) async {
// Payment result sent successfully, now fetch purchase history
await offersAndDiscountsViewModel.getOffersAndDiscountsHistory(
onSuccess: (historyResponse) {
LoaderBottomSheet.hideLoader();
// Find the purchased offer in the history by matching the offer ID
final purchaseData = offersAndDiscountsViewModel.historyList.firstWhere(
(item) => item.offer.id == selectedOffer.id,
orElse: () => offersAndDiscountsViewModel.historyList.first,
);
// Navigate to success QR page with purchase data
Navigator.of(context).pushReplacement(
CustomPageRoute(
page: OffersAndDiscountsSuccessQrPage(purchaseData: purchaseData),
),
);
},
onError: (error) {
// If history fetch fails, still show success message
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.paymentSuccessful.tr(context: context)),
callBackFunc: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
},
onError: (error) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Payment recorded but confirmation failed. Please contact support."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}
}

@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
@ -99,7 +100,7 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
return Center(child: Text(LocaleKeys.noDataAvailable.tr(context: context)));
}
print(offer.toJson());
print(vm.offerDetails!.toRawJson());
final status = _getOfferStatus(offer.validTo);
return SingleChildScrollView(
@ -293,7 +294,28 @@ class _OffersAndDiscountsDetailedPageState extends State<OffersAndDiscountsDetai
text: "Buy Now",
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () {
onPressed: () async {
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await viewModel.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
return;
}
// Set the selected offer and navigate to payment page
viewModel.setSelectedOffer(offer);
Navigator.of(context).push(

@ -5,10 +5,16 @@ import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_success_payment_qr_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
class OffersAndDiscountsHistoryPage extends StatefulWidget {
@ -25,7 +31,7 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
@override
void initState() {
scheduleMicrotask(() {
// offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
});
super.initState();
}
@ -36,42 +42,142 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
return CollapsingListView(
title: '${LocaleKeys.order.tr(context: context)} ${LocaleKeys.history.tr(context: context)}',
title: LocaleKeys.order.tr(context: context),
child: SingleChildScrollView(
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
if (offersAndDiscountVM.isHistoryLoading) {
return Container(
padding: EdgeInsets.all(24.h),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
),
),
);
}
final historyList = offersAndDiscountVM.filteredHistory;
if (historyList.isEmpty) {
return Container(
padding: EdgeInsets.all(24.h),
child: Column(
children: [
Icon(
Icons.history,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
LocaleKeys.noDataAvailable.tr(context: context).toText18(isBold: true),
SizedBox(height: 8.h),
"You haven't purchased any offers yet.".toText14(color: AppColors.greyTextColor),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Temporarily show a message that history is not available yet
Container(
padding: EdgeInsets.all(24.h),
child: Column(
children: [
Icon(
Icons.history,
size: 64.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 16.h),
Text(
'Order history coming soon',
style: TextStyle(
fontSize: 18.f,
fontWeight: FontWeight.w700,
color: AppColors.textColor,
),
),
SizedBox(height: 8.h),
Text(
'This feature will be available once the history API is ready.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.f,
color: AppColors.greyTextColor,
),
),
],
),
SizedBox(height: 24.h),
ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: historyList.length,
separatorBuilder: (context, index) => SizedBox(height: 16.h),
itemBuilder: (context, index) {
final history = historyList[index];
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomChipWidget(
labelText: _getOfferStatus(history),
backgroundColor: _getOfferStatusBgColor(history),
textColor: _getOfferStatusTextColor(history),
labelPadding: EdgeInsetsDirectional.only(start: 12.w, end: 12.w, top: 2.h, bottom: 2.h),
),
SizedBox(width: 8.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: (appState.getLanguageID() == 1 ? history.offer.nameAr : history.offer.name).toText16(isBold: true),
),
],
),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
AppCustomChipWidget(
labelText: "Order: ${history.voucherCode}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Purchased: ${_formatDateChipFromDateTime(history.purchaseDate)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
AppCustomChipWidget(
labelText: "Valid until: ${_formatDateChipFromDateTime(history.validTo)}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
isEnglishOnly: true,
),
AppCustomChipWidget(
labelText:
"Hospital: ${appState.getLanguageID() == 1 ? history.branches.first.nameAr : history.branches.first.name} ${history.branches.length > 1 ? '(+${history.branches.length - 1} more)' : ''}",
backgroundColor: AppColors.chipBgColor,
textColor: AppColors.greyTextColor,
icon: "",
labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w),
),
],
),
SizedBox(height: 12.h),
Row(
children: [
Expanded(
child: CustomButton(
text: "View Offer Details",
onPressed: () {},
backgroundColor: AppColors.lightRedButtonColor,
borderColor: AppColors.lightRedButtonColor,
textColor: AppColors.primaryRedColor,
borderWidth: 0.w,
),
),
SizedBox(
width: 9.w,
),
Expanded(
child: CustomButton(
text: "View Order Details",
onPressed: () {
Navigator.of(context).push(CustomPageRoute(page: OffersAndDiscountsSuccessQrPage(purchaseData: history)));
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.textColor,
textColor: AppColors.textColor,
),
)
],
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h);
},
),
SizedBox(height: 24.h),
],
@ -80,4 +186,94 @@ class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistor
),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'completed':
case 'success':
case 'paid':
return AppColors.lightGreenColor;
case 'pending':
return AppColors.alertColor;
case 'cancelled':
case 'failed':
return AppColors.errorColor;
default:
return AppColors.greyTextColor;
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'active':
case 'valid':
return LocaleKeys.active.tr(context: context);
case 'redeemed':
case 'used':
return "Redeemed";
case 'expired':
return LocaleKeys.expired.tr(context: context);
case 'pending':
return LocaleKeys.pending.tr(context: context);
case 'cancelled':
return LocaleKeys.cancelled.tr(context: context);
default:
return status.toUpperCase();
}
}
String _formatDateChipFromDateTime(DateTime date) {
try {
return DateFormat('d MMM yyyy').format(date);
} catch (e) {
return date.toString();
}
}
String _getOfferStatus(dynamic history) {
try {
final now = DateTime.now();
// Check if the offer has expired based on validTo date
if (now.isAfter(history.validTo)) {
return LocaleKeys.expired.tr(context: context);
}
// Check state for active/available status
if (history.state.toLowerCase() == 'redeemed' || history.state.toLowerCase() == 'used') {
return LocaleKeys.expired.tr(context: context);
}
if (history.state.toLowerCase() == 'active' || history.state.toLowerCase() == 'valid') {
return LocaleKeys.active.tr(context: context);
}
// For pending or other states, show as available
return "Available";
} catch (e) {
return LocaleKeys.active.tr(context: context);
}
}
Color _getOfferStatusBgColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
return AppColors.successColor.withValues(alpha: 0.1);
} else if (status == LocaleKeys.expired.tr(context: context)) {
return AppColors.errorColor.withValues(alpha: 0.1);
} else {
return AppColors.alertColor.withValues(alpha: 0.1);
}
}
Color _getOfferStatusTextColor(dynamic history) {
final status = _getOfferStatus(history);
if (status == LocaleKeys.active.tr(context: context)) {
return AppColors.successColor;
} else if (status == LocaleKeys.expired.tr(context: context)) {
return AppColors.errorColor;
} else {
return AppColors.alertColor;
}
}
}

@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
@ -41,10 +42,29 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
@override
void initState() {
scheduleMicrotask(() {
offersAndDiscountsViewModel.initOffersAndDiscounts();
});
super.initState();
appState = getIt.get<AppState>();
scheduleMicrotask(() async {
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
// Authenticate patient once when visiting the offers page
if (!appState.isPomsAuthTokenValid()) {
await offersAndDiscountsViewModel.authenticatePatient(
onSuccess: (authToken) {
print("POMS Authentication successful: ${authToken.token}");
},
onError: (error) {
print("POMS Authentication failed: $error");
// Authentication failed, but user can still browse offers
},
);
}
// Load offers only if we don't have data yet (prevents double API call)
if (offersAndDiscountsViewModel.offersList.isEmpty) {
offersAndDiscountsViewModel.initOffersAndDiscounts();
}
});
}
@override
@ -294,12 +314,37 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w),
),
SizedBox(height: 20.h),
CustomButton(
CustomButton(
text: 'Buy Now',
icon: AppAssets.shoppingCart,
iconColor: AppColors.whiteColor,
onPressed: () async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.pleaseWait.tr(context: context));
// Ensure patient is authenticated (will re-authenticate if needed)
final isAuthenticated = await offersAndDiscountVM.ensureAuthenticated(
onSuccess: () {
// Authentication successful, continue
},
onError: (error) {
LoaderBottomSheet.hideLoader();
// Show error to user
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
if (!isAuthenticated) {
LoaderBottomSheet.hideLoader();
return;
}
// Fetch offer details
await offersAndDiscountVM.getOfferDetails(
offersAndDiscountVM.filteredOffers[index].id,
onSuccess: (response) {

@ -30,10 +30,13 @@ class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel>
void initState() {
super.initState();
appState = getIt.get<AppState>();
// Initialize the view model and load offers data
// Initialize the view model and load offers data only if not already loaded
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
viewModel.initOffersAndDiscounts();
// Only initialize if we don't have data yet
if (viewModel.offersList.isEmpty) {
viewModel.initOffersAndDiscounts();
}
});
// _startAutoScroll();
}

Loading…
Cancel
Save