From 4728df93e28dc4bb80419ee4725729bbe4b3f300 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 22 Jun 2025 17:55:24 +0800 Subject: [PATCH] :sparkles: Solarpay sheet --- assets/i18n/en-US.json | 24 +- assets/i18n/zh-CN.json | 8 +- ios/Podfile.lock | 13 + lib/models/user.dart | 2 + lib/models/user.freezed.dart | 59 ++- lib/models/user.g.dart | 7 + lib/models/wallet.dart | 26 + lib/models/wallet.freezed.dart | 235 +++++++++ lib/models/wallet.g.dart | 53 ++ lib/screens/account/leveling.dart | 395 +++++++++++++- lib/services/responsive.dart | 4 +- lib/widgets/alert.dart | 19 +- lib/widgets/payment/README.md | 243 +++++++++ lib/widgets/payment/payment_overlay.dart | 495 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 ++- pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 21 files changed, 1604 insertions(+), 56 deletions(-) create mode 100644 lib/widgets/payment/README.md create mode 100644 lib/widgets/payment/payment_overlay.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index e076c0b..a30f643 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -78,7 +78,6 @@ "exploreFilterFriends": "Friends", "account": "Account", "name": "Name", - "description": "Description", "slug": "Slug", "slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.", "createChatRoom": "Create a Room", @@ -467,5 +466,26 @@ "preview": "Preview", "togglePreview": "Toggle Preview", "subscribe": "Subscribe", - "unsubscribe": "Unsubscribe" + "unsubscribe": "Unsubscribe", + "paymentVerification": "Payment Verification", + "paymentSummary": "Payment Summary", + "amount": "Amount", + "description": "Description", + "pinCode": "PIN Code", + "biometric": "Biometric", + "enterPinToConfirm": "Enter your 6-digit PIN to confirm payment", + "clearPin": "Clear PIN", + "useBiometricToConfirm": "Use biometric authentication to confirm payment", + "touchSensorToAuthenticate": "Touch the sensor to authenticate", + "authenticating": "Authenticating...", + "authenticateNow": "Authenticate Now", + "processing": "Processing...", + "processingPayment": "Processing Payment...", + "pleaseWait": "Please wait", + "paymentFailed": "Payment failed. Please try again.", + "invalidPin": "Invalid PIN. Please try again.", + "biometricAuthFailed": "Biometric authentication failed. Please try again.", + "paymentSuccess": "Payment completed successfully!", + "membershipPurchaseSuccess": "Membership purchased successfully!", + "paymentError": "Payment failed: {error}" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 429fbab..81a718e 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -313,5 +313,11 @@ "checkInResultT1": "凶", "checkInResultT2": "中平", "checkInResultT3": "吉", - "checkInResultT4": "大吉" + "checkInResultT4": "大吉", + "authenticating": "认证中...", + "processing": "处理中...", + "processingPayment": "处理付款中...", + "pleaseWait": "请稍候", + "paymentFailed": "付款失败,请重试。", + "paymentSuccess": "付款成功完成!" } \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0bf9654..47d08b9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -84,6 +84,8 @@ PODS: - Flutter - flutter_platform_alert (0.0.1): - Flutter + - flutter_secure_storage (3.3.1): + - Flutter - flutter_timezone (0.0.1): - Flutter - flutter_udid (0.0.1): @@ -131,6 +133,9 @@ PODS: - Flutter - flutter_webrtc - WebRTC-SDK (= 125.6422.07) + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_video (0.0.1): @@ -210,6 +215,7 @@ DEPENDENCIES: - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) @@ -218,6 +224,7 @@ DEPENDENCIES: - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - Kingfisher (~> 8.0) - livekit_client (from `.symlinks/plugins/livekit_client/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - native_exif (from `.symlinks/plugins/native_exif/ios`) @@ -277,6 +284,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_platform_alert: :path: ".symlinks/plugins/flutter_platform_alert/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_timezone: :path: ".symlinks/plugins/flutter_timezone/ios" flutter_udid: @@ -291,6 +300,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/irondash_engine_context/ios" livekit_client: :path: ".symlinks/plugins/livekit_client/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" media_kit_video: @@ -341,6 +352,7 @@ SPEC CHECKSUMS: flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 + flutter_secure_storage: 50035aef357c5a8bdd67fd6bc81370d46efc4d16 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 @@ -351,6 +363,7 @@ SPEC CHECKSUMS: irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5 livekit_client: 9e901890552514206e5ff828903ed271531da264 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 diff --git a/lib/models/user.dart b/lib/models/user.dart index ac3b45b..4fee4be 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:island/models/file.dart'; +import 'package:island/models/wallet.dart'; part 'user.freezed.dart'; part 'user.g.dart'; @@ -44,6 +45,7 @@ sealed class SnAccountProfile with _$SnAccountProfile { required SnCloudFile? picture, required SnCloudFile? background, required SnVerificationMark? verification, + required SnWalletSubscription? stellarMembership, required DateTime createdAt, required DateTime updatedAt, required DateTime? deletedAt, diff --git a/lib/models/user.freezed.dart b/lib/models/user.freezed.dart index 2545102..11f2502 100644 --- a/lib/models/user.freezed.dart +++ b/lib/models/user.freezed.dart @@ -200,7 +200,7 @@ $SnAccountProfileCopyWith<$Res> get profile { /// @nodoc mixin _$SnAccountProfile { - String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; SnWalletSubscription? get stellarMembership; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -213,16 +213,16 @@ $SnAccountProfileCopyWith get copyWith => _$SnAccountProfileCo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.stellarMembership, stellarMembership) || other.stellarMembership == stellarMembership)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,stellarMembership,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, stellarMembership: $stellarMembership, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -233,11 +233,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> { factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; @useResult $Res call({ - String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, SnWalletSubscription? stellarMembership, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification; +$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$SnWalletSubscriptionCopyWith<$Res>? get stellarMembership; } /// @nodoc @@ -250,7 +250,7 @@ class _$SnAccountProfileCopyWithImpl<$Res> /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? stellarMembership = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable @@ -270,7 +270,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable -as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,stellarMembership: freezed == stellarMembership ? _self.stellarMembership : stellarMembership // ignore: cast_nullable_to_non_nullable +as SnWalletSubscription?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?, @@ -324,6 +325,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { return _then(_self.copyWith(verification: value)); }); +}/// Create a copy of SnAccountProfile +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletSubscriptionCopyWith<$Res>? get stellarMembership { + if (_self.stellarMembership == null) { + return null; + } + + return $SnWalletSubscriptionCopyWith<$Res>(_self.stellarMembership!, (value) { + return _then(_self.copyWith(stellarMembership: value)); + }); } } @@ -332,7 +345,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification { @JsonSerializable() class _SnAccountProfile implements SnAccountProfile { - const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}); + const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.stellarMembership, required this.createdAt, required this.updatedAt, required this.deletedAt}); factory _SnAccountProfile.fromJson(Map json) => _$SnAccountProfileFromJson(json); @override final String id; @@ -353,6 +366,7 @@ class _SnAccountProfile implements SnAccountProfile { @override final SnCloudFile? picture; @override final SnCloudFile? background; @override final SnVerificationMark? verification; +@override final SnWalletSubscription? stellarMembership; @override final DateTime createdAt; @override final DateTime updatedAt; @override final DateTime? deletedAt; @@ -370,16 +384,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.stellarMembership, stellarMembership) || other.stellarMembership == stellarMembership)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,stellarMembership,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, stellarMembership: $stellarMembership, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -390,11 +404,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; @override @useResult $Res call({ - String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, SnWalletSubscription? stellarMembership, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification; +@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $SnWalletSubscriptionCopyWith<$Res>? get stellarMembership; } /// @nodoc @@ -407,7 +421,7 @@ class __$SnAccountProfileCopyWithImpl<$Res> /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? stellarMembership = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnAccountProfile( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable @@ -427,7 +441,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable -as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,stellarMembership: freezed == stellarMembership ? _self.stellarMembership : stellarMembership // ignore: cast_nullable_to_non_nullable +as SnWalletSubscription?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?, @@ -482,6 +497,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { return _then(_self.copyWith(verification: value)); }); +}/// Create a copy of SnAccountProfile +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletSubscriptionCopyWith<$Res>? get stellarMembership { + if (_self.stellarMembership == null) { + return null; + } + + return $SnWalletSubscriptionCopyWith<$Res>(_self.stellarMembership!, (value) { + return _then(_self.copyWith(stellarMembership: value)); + }); } } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 6668dfa..5fd7f65 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -84,6 +84,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map json) => : SnVerificationMark.fromJson( json['verification'] as Map, ), + stellarMembership: + json['stellar_membership'] == null + ? null + : SnWalletSubscription.fromJson( + json['stellar_membership'] as Map, + ), createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), deletedAt: @@ -112,6 +118,7 @@ Map _$SnAccountProfileToJson(_SnAccountProfile instance) => 'picture': instance.picture?.toJson(), 'background': instance.background?.toJson(), 'verification': instance.verification?.toJson(), + 'stellar_membership': instance.stellarMembership?.toJson(), 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), diff --git a/lib/models/wallet.dart b/lib/models/wallet.dart index 505d84a..f2dd341 100644 --- a/lib/models/wallet.dart +++ b/lib/models/wallet.dart @@ -85,3 +85,29 @@ sealed class SnWalletSubscription with _$SnWalletSubscription { factory SnWalletSubscription.fromJson(Map json) => _$SnWalletSubscriptionFromJson(json); } + +@freezed +sealed class SnWalletOrder with _$SnWalletOrder { + const factory SnWalletOrder({ + required String id, + required int status, + required String currency, + required dynamic remarks, + required String appIdentifier, + @Default({}) Map meta, + required int amount, + required DateTime expiredAt, + required String? payeeWalletId, + required SnWallet? payeeWallet, + required String? transactionId, + required SnTransaction? transaction, + required String? issuerAppId, + required dynamic issuerApp, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + }) = _SnWalletOrder; + + factory SnWalletOrder.fromJson(Map json) => + _$SnWalletOrderFromJson(json); +} diff --git a/lib/models/wallet.freezed.dart b/lib/models/wallet.freezed.dart index 71415d7..2782f55 100644 --- a/lib/models/wallet.freezed.dart +++ b/lib/models/wallet.freezed.dart @@ -778,4 +778,239 @@ $SnAccountCopyWith<$Res>? get account { } } + +/// @nodoc +mixin _$SnWalletOrder { + + String get id; int get status; String get currency; dynamic get remarks; String get appIdentifier; Map get meta; int get amount; DateTime get expiredAt; String? get payeeWalletId; SnWallet? get payeeWallet; String? get transactionId; SnTransaction? get transaction; String? get issuerAppId; dynamic get issuerApp; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnWalletOrderCopyWith get copyWith => _$SnWalletOrderCopyWithImpl(this as SnWalletOrder, _$identity); + + /// Serializes this SnWalletOrder to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletOrder&&(identical(other.id, id) || other.id == id)&&(identical(other.status, status) || other.status == status)&&(identical(other.currency, currency) || other.currency == currency)&&const DeepCollectionEquality().equals(other.remarks, remarks)&&(identical(other.appIdentifier, appIdentifier) || other.appIdentifier == appIdentifier)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.transactionId, transactionId) || other.transactionId == transactionId)&&(identical(other.transaction, transaction) || other.transaction == transaction)&&(identical(other.issuerAppId, issuerAppId) || other.issuerAppId == issuerAppId)&&const DeepCollectionEquality().equals(other.issuerApp, issuerApp)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,status,currency,const DeepCollectionEquality().hash(remarks),appIdentifier,const DeepCollectionEquality().hash(meta),amount,expiredAt,payeeWalletId,payeeWallet,transactionId,transaction,issuerAppId,const DeepCollectionEquality().hash(issuerApp),createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletOrder(id: $id, status: $status, currency: $currency, remarks: $remarks, appIdentifier: $appIdentifier, meta: $meta, amount: $amount, expiredAt: $expiredAt, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, transactionId: $transactionId, transaction: $transaction, issuerAppId: $issuerAppId, issuerApp: $issuerApp, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnWalletOrderCopyWith<$Res> { + factory $SnWalletOrderCopyWith(SnWalletOrder value, $Res Function(SnWalletOrder) _then) = _$SnWalletOrderCopyWithImpl; +@useResult +$Res call({ + String id, int status, String currency, dynamic remarks, String appIdentifier, Map meta, int amount, DateTime expiredAt, String? payeeWalletId, SnWallet? payeeWallet, String? transactionId, SnTransaction? transaction, String? issuerAppId, dynamic issuerApp, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +$SnWalletCopyWith<$Res>? get payeeWallet;$SnTransactionCopyWith<$Res>? get transaction; + +} +/// @nodoc +class _$SnWalletOrderCopyWithImpl<$Res> + implements $SnWalletOrderCopyWith<$Res> { + _$SnWalletOrderCopyWithImpl(this._self, this._then); + + final SnWalletOrder _self; + final $Res Function(SnWalletOrder) _then; + +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? status = null,Object? currency = null,Object? remarks = freezed,Object? appIdentifier = null,Object? meta = null,Object? amount = null,Object? expiredAt = null,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? transactionId = freezed,Object? transaction = freezed,Object? issuerAppId = freezed,Object? issuerApp = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable +as String,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable +as dynamic,appIdentifier: null == appIdentifier ? _self.appIdentifier : appIdentifier // ignore: cast_nullable_to_non_nullable +as String,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable +as Map,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as int,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable +as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,transactionId: freezed == transactionId ? _self.transactionId : transactionId // ignore: cast_nullable_to_non_nullable +as String?,transaction: freezed == transaction ? _self.transaction : transaction // ignore: cast_nullable_to_non_nullable +as SnTransaction?,issuerAppId: freezed == issuerAppId ? _self.issuerAppId : issuerAppId // ignore: cast_nullable_to_non_nullable +as String?,issuerApp: freezed == issuerApp ? _self.issuerApp : issuerApp // ignore: cast_nullable_to_non_nullable +as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payeeWallet { + if (_self.payeeWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) { + return _then(_self.copyWith(payeeWallet: value)); + }); +}/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnTransactionCopyWith<$Res>? get transaction { + if (_self.transaction == null) { + return null; + } + + return $SnTransactionCopyWith<$Res>(_self.transaction!, (value) { + return _then(_self.copyWith(transaction: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _SnWalletOrder implements SnWalletOrder { + const _SnWalletOrder({required this.id, required this.status, required this.currency, required this.remarks, required this.appIdentifier, final Map meta = const {}, required this.amount, required this.expiredAt, required this.payeeWalletId, required this.payeeWallet, required this.transactionId, required this.transaction, required this.issuerAppId, required this.issuerApp, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; + factory _SnWalletOrder.fromJson(Map json) => _$SnWalletOrderFromJson(json); + +@override final String id; +@override final int status; +@override final String currency; +@override final dynamic remarks; +@override final String appIdentifier; + final Map _meta; +@override@JsonKey() Map get meta { + if (_meta is EqualUnmodifiableMapView) return _meta; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_meta); +} + +@override final int amount; +@override final DateTime expiredAt; +@override final String? payeeWalletId; +@override final SnWallet? payeeWallet; +@override final String? transactionId; +@override final SnTransaction? transaction; +@override final String? issuerAppId; +@override final dynamic issuerApp; +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnWalletOrderCopyWith<_SnWalletOrder> get copyWith => __$SnWalletOrderCopyWithImpl<_SnWalletOrder>(this, _$identity); + +@override +Map toJson() { + return _$SnWalletOrderToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletOrder&&(identical(other.id, id) || other.id == id)&&(identical(other.status, status) || other.status == status)&&(identical(other.currency, currency) || other.currency == currency)&&const DeepCollectionEquality().equals(other.remarks, remarks)&&(identical(other.appIdentifier, appIdentifier) || other.appIdentifier == appIdentifier)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.transactionId, transactionId) || other.transactionId == transactionId)&&(identical(other.transaction, transaction) || other.transaction == transaction)&&(identical(other.issuerAppId, issuerAppId) || other.issuerAppId == issuerAppId)&&const DeepCollectionEquality().equals(other.issuerApp, issuerApp)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,status,currency,const DeepCollectionEquality().hash(remarks),appIdentifier,const DeepCollectionEquality().hash(_meta),amount,expiredAt,payeeWalletId,payeeWallet,transactionId,transaction,issuerAppId,const DeepCollectionEquality().hash(issuerApp),createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletOrder(id: $id, status: $status, currency: $currency, remarks: $remarks, appIdentifier: $appIdentifier, meta: $meta, amount: $amount, expiredAt: $expiredAt, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, transactionId: $transactionId, transaction: $transaction, issuerAppId: $issuerAppId, issuerApp: $issuerApp, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnWalletOrderCopyWith<$Res> implements $SnWalletOrderCopyWith<$Res> { + factory _$SnWalletOrderCopyWith(_SnWalletOrder value, $Res Function(_SnWalletOrder) _then) = __$SnWalletOrderCopyWithImpl; +@override @useResult +$Res call({ + String id, int status, String currency, dynamic remarks, String appIdentifier, Map meta, int amount, DateTime expiredAt, String? payeeWalletId, SnWallet? payeeWallet, String? transactionId, SnTransaction? transaction, String? issuerAppId, dynamic issuerApp, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +@override $SnWalletCopyWith<$Res>? get payeeWallet;@override $SnTransactionCopyWith<$Res>? get transaction; + +} +/// @nodoc +class __$SnWalletOrderCopyWithImpl<$Res> + implements _$SnWalletOrderCopyWith<$Res> { + __$SnWalletOrderCopyWithImpl(this._self, this._then); + + final _SnWalletOrder _self; + final $Res Function(_SnWalletOrder) _then; + +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? status = null,Object? currency = null,Object? remarks = freezed,Object? appIdentifier = null,Object? meta = null,Object? amount = null,Object? expiredAt = null,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? transactionId = freezed,Object? transaction = freezed,Object? issuerAppId = freezed,Object? issuerApp = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnWalletOrder( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable +as String,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable +as dynamic,appIdentifier: null == appIdentifier ? _self.appIdentifier : appIdentifier // ignore: cast_nullable_to_non_nullable +as String,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable +as Map,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as int,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable +as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable +as SnWallet?,transactionId: freezed == transactionId ? _self.transactionId : transactionId // ignore: cast_nullable_to_non_nullable +as String?,transaction: freezed == transaction ? _self.transaction : transaction // ignore: cast_nullable_to_non_nullable +as SnTransaction?,issuerAppId: freezed == issuerAppId ? _self.issuerAppId : issuerAppId // ignore: cast_nullable_to_non_nullable +as String?,issuerApp: freezed == issuerApp ? _self.issuerApp : issuerApp // ignore: cast_nullable_to_non_nullable +as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnWalletCopyWith<$Res>? get payeeWallet { + if (_self.payeeWallet == null) { + return null; + } + + return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) { + return _then(_self.copyWith(payeeWallet: value)); + }); +}/// Create a copy of SnWalletOrder +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnTransactionCopyWith<$Res>? get transaction { + if (_self.transaction == null) { + return null; + } + + return $SnTransactionCopyWith<$Res>(_self.transaction!, (value) { + return _then(_self.copyWith(transaction: value)); + }); +} +} + // dart format on diff --git a/lib/models/wallet.g.dart b/lib/models/wallet.g.dart index 9dd3904..bba682c 100644 --- a/lib/models/wallet.g.dart +++ b/lib/models/wallet.g.dart @@ -156,3 +156,56 @@ Map _$SnWalletSubscriptionToJson( 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnWalletOrder _$SnWalletOrderFromJson(Map json) => + _SnWalletOrder( + id: json['id'] as String, + status: (json['status'] as num).toInt(), + currency: json['currency'] as String, + remarks: json['remarks'], + appIdentifier: json['app_identifier'] as String, + meta: json['meta'] as Map? ?? const {}, + amount: (json['amount'] as num).toInt(), + expiredAt: DateTime.parse(json['expired_at'] as String), + payeeWalletId: json['payee_wallet_id'] as String?, + payeeWallet: + json['payee_wallet'] == null + ? null + : SnWallet.fromJson(json['payee_wallet'] as Map), + transactionId: json['transaction_id'] as String?, + transaction: + json['transaction'] == null + ? null + : SnTransaction.fromJson( + json['transaction'] as Map, + ), + issuerAppId: json['issuer_app_id'] as String?, + issuerApp: json['issuer_app'], + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: + json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + ); + +Map _$SnWalletOrderToJson(_SnWalletOrder instance) => + { + 'id': instance.id, + 'status': instance.status, + 'currency': instance.currency, + 'remarks': instance.remarks, + 'app_identifier': instance.appIdentifier, + 'meta': instance.meta, + 'amount': instance.amount, + 'expired_at': instance.expiredAt.toIso8601String(), + 'payee_wallet_id': instance.payeeWalletId, + 'payee_wallet': instance.payeeWallet?.toJson(), + 'transaction_id': instance.transactionId, + 'transaction': instance.transaction?.toJson(), + 'issuer_app_id': instance.issuerAppId, + 'issuer_app': instance.issuerApp, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + }; diff --git a/lib/screens/account/leveling.dart b/lib/screens/account/leveling.dart index c27e9e4..116e7dc 100644 --- a/lib/screens/account/leveling.dart +++ b/lib/screens/account/leveling.dart @@ -1,13 +1,19 @@ import 'package:auto_route/auto_route.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/pods/userinfo.dart'; -import 'package:island/services/responsive.dart'; -import 'package:island/widgets/app_scaffold.dart'; -import 'package:island/widgets/account/leveling_progress.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/user.dart'; +import 'package:island/models/wallet.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/pods/userinfo.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/account/leveling_progress.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/payment/payment_overlay.dart'; +import 'package:easy_localization/easy_localization.dart'; @RoutePage() class LevelingScreen extends HookConsumerWidget { @@ -57,30 +63,33 @@ class LevelingScreen extends HookConsumerWidget { const Gap(24), - // Placeholder for unlocked content - Text( - 'Unlocked Features', - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), - ), + // Membership section + _buildMembershipSection(context, ref, user.value!), const Gap(16), + + // Unlocked features section Container( - height: 200, + width: double.infinity, + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, + color: Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - ), ), - child: Center( - child: Text( - 'Unlocked features will be shown here', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Unlocked Features', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - ), + const Gap(8), + Text( + 'Features unlocked at your current level will be displayed here.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], ), ), ], @@ -208,6 +217,344 @@ class LevelingScreen extends HookConsumerWidget { ), ); } + + Widget _buildMembershipSection( + BuildContext context, + WidgetRef ref, + SnAccount user, + ) { + final membership = user.profile.stellarMembership; + final isActive = membership?.isActive ?? false; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primaryContainer, + Theme.of(context).colorScheme.secondaryContainer, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isActive ? Icons.star : Icons.star_border, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + const Gap(8), + Text( + 'Stellar Membership', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const Gap(12), + + if (isActive) ...[ + _buildCurrentMembershipCard(context, membership!), + const Gap(16), + ], + + Text( + isActive ? 'Upgrade Your Plan' : 'Choose Your Plan', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const Gap(12), + + _buildMembershipTiers(context, ref, membership), + ], + ), + ); + } + + Widget _buildCurrentMembershipCard( + BuildContext context, + SnWalletSubscription membership, + ) { + final tierName = _getMembershipTierName(membership.identifier); + final tierColor = _getMembershipTierColor(context, membership.identifier); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: tierColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: tierColor, width: 1), + ), + child: Row( + children: [ + Icon(Icons.verified, color: tierColor, size: 20), + const Gap(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current: $tierName', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: tierColor, + ), + ), + Text( + 'Expires: ${_formatDate(membership.endedAt)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMembershipTiers( + BuildContext context, + WidgetRef ref, + SnWalletSubscription? currentMembership, + ) { + final tiers = [ + { + 'id': 'solian.stellar.primary', + 'name': 'Stellar', + 'price': '10 NS\$ per month', + 'features': [ + 'Basic features', + 'Priority support', + 'Ad-free experience', + ], + 'color': Colors.blue, + }, + { + 'id': 'solian.stellar.nova', + 'name': 'Nova', + 'price': '20 NS\$ per month', + 'features': [ + 'All Primary features', + 'Advanced customization', + 'Early access', + ], + 'color': Colors.purple, + }, + { + 'id': 'solian.stellar.supernova', + 'name': 'Supernova', + 'price': '30 NS\$ per month', + 'features': ['All Nova features', 'Exclusive content', 'VIP support'], + 'color': Colors.orange, + }, + ]; + + return Column( + children: + tiers.map((tier) { + final isCurrentTier = currentMembership?.identifier == tier['id']; + final tierColor = tier['color'] as Color; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: + isCurrentTier + ? null + : () => _purchaseMembership( + context, + ref, + tier['id'] as String, + ), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: + isCurrentTier + ? tierColor.withOpacity(0.1) + : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + isCurrentTier + ? tierColor + : Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: isCurrentTier ? 2 : 1, + ), + ), + child: Row( + children: [ + Container( + width: 4, + height: 40, + decoration: BoxDecoration( + color: tierColor, + borderRadius: BorderRadius.circular(2), + ), + ), + const Gap(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + tier['name'] as String, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isCurrentTier ? tierColor : null, + ), + ), + const Gap(8), + if (isCurrentTier) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: tierColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'CURRENT', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + Text( + tier['price'] as String, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (!isCurrentTier) + Icon( + Icons.arrow_forward_ios, + size: 16, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ); + } + + String _getMembershipTierName(String identifier) { + switch (identifier) { + case 'solian.stellar.primary': + return 'Primary'; + case 'solian.stellar.nova': + return 'Nova'; + case 'solian.stellar.supernova': + return 'Supernova'; + default: + return 'Unknown'; + } + } + + Color _getMembershipTierColor(BuildContext context, String identifier) { + switch (identifier) { + case 'solian.stellar.primary': + return Colors.blue; + case 'solian.stellar.nova': + return Colors.purple; + case 'solian.stellar.supernova': + return Colors.orange; + default: + return Theme.of(context).colorScheme.primary; + } + } + + String _formatDate(DateTime date) { + return '${date.day}/${date.month}/${date.year}'; + } + + Future _purchaseMembership( + BuildContext context, + WidgetRef ref, + String tierId, + ) async { + final client = ref.watch(apiClientProvider); + try { + showLoadingModal(context); + final resp = await client.post( + '/subscriptions', + data: { + 'identifier': tierId, + 'payment_method': 'solian.wallet', + 'payment_details': {'currency': 'golds'}, + 'cycle_duration_days': 30, + }, + options: Options(headers: {'X-Noop': true}), + ); + final subscription = SnWalletSubscription.fromJson(resp.data); + if (subscription.status == 1) return; + final orderResp = await client.post( + '/subscriptions/${subscription.identifier}/order', + ); + final order = SnWalletOrder.fromJson(orderResp.data); + + if (context.mounted) hideLoadingModal(context); + + // Show payment overlay to complete the payment + if (!context.mounted) return; + final paidOrder = await PaymentOverlay.show( + context: context, + order: order, + enableBiometric: true, + ); + + if (paidOrder != null) { + // Payment successful, refresh user info or show success message + ref.invalidate(userInfoProvider); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('membershipPurchaseSuccess'.tr()), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } + } + } catch (err) { + if (context.mounted) hideLoadingModal(context); + showErrorAlert(err); + } + } } class LevelStairsPainter extends CustomPainter { diff --git a/lib/services/responsive.dart b/lib/services/responsive.dart index 2555816..7f55b30 100644 --- a/lib/services/responsive.dart +++ b/lib/services/responsive.dart @@ -40,7 +40,7 @@ EdgeInsets getTabbedPadding( top: top ?? vertical ?? 0, bottom: effectiveBottom != null - ? effectiveBottom + MediaQuery.of(context).padding.bottom + 16 - : MediaQuery.of(context).padding.bottom + 16, + ? effectiveBottom + MediaQuery.of(context).padding.bottom + 56 + : MediaQuery.of(context).padding.bottom + 56, ); } diff --git a/lib/widgets/alert.dart b/lib/widgets/alert.dart index b314987..c6232f4 100644 --- a/lib/widgets/alert.dart +++ b/lib/widgets/alert.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; +import 'package:island/services/responsive.dart'; import 'package:styled_widget/styled_widget.dart'; export 'content/alert.native.dart' @@ -11,9 +12,21 @@ void showSnackBar( String message, { SnackBarAction? action, }) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message), action: action)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + action: action, + margin: + isWideScreen(context) + ? null + : EdgeInsets.fromLTRB( + 15.0, + 5.0, + 15.0, + MediaQuery.of(context).padding.bottom + 28, + ), + ), + ); } void clearSnackBar(BuildContext context) { diff --git a/lib/widgets/payment/README.md b/lib/widgets/payment/README.md new file mode 100644 index 0000000..645502d --- /dev/null +++ b/lib/widgets/payment/README.md @@ -0,0 +1,243 @@ +# Payment Overlay Widget + +A reusable payment verification overlay that supports both 6-digit PIN input and biometric authentication for secure payment processing. + +## Features + +- **6-digit PIN Input**: Secure numeric PIN entry with automatic focus management +- **Biometric Authentication**: Support for fingerprint and face recognition +- **Order Summary**: Display payment details including amount, description, and remarks +- **Integrated API Calls**: Automatically handles payment processing via `/orders/{orderId}/pay` +- **Error Handling**: Comprehensive error handling with user-friendly messages +- **Loading States**: Visual feedback during payment processing +- **Responsive Design**: Adapts to different screen sizes and orientations +- **Customizable**: Flexible callbacks and styling options +- **Accessibility**: Screen reader support and proper focus management +- **Localization**: Full i18n support with easy_localization + +## Usage + +```dart +import 'package:flutter/material.dart'; +import 'package:solian/models/wallet.dart'; +import 'package:solian/widgets/payment/payment_overlay.dart'; + +// Create an order +final order = SnWalletOrder( + id: 'order_123', + amount: 2500, // $25.00 in cents + currency: 'USD', + description: 'Premium Subscription', + remarks: 'Monthly billing', + status: 'pending', +); + +// Show payment overlay +PaymentOverlay.show( + context: context, + order: order, + onPaymentSuccess: (completedOrder) { + // Handle successful payment + print('Payment completed: ${completedOrder.id}'); + // Navigate to success page or update UI + }, + onPaymentError: (error) { + // Handle payment error + print('Payment failed: $error'); + // Show error message to user + }, + onCancel: () { + Navigator.of(context).pop(); + print('Payment cancelled'); + }, + enableBiometric: true, +); +``` + +### Advanced Usage with Loading States + +```dart +bool isLoading = false; + +PaymentOverlay.show( + context: context, + order: order, + enableBiometric: true, + isLoading: isLoading, + onPinSubmit: (String pin) async { + setState(() => isLoading = true); + try { + await processPaymentWithPin(pin); + Navigator.of(context).pop(); + } catch (e) { + showErrorDialog(e.toString()); + } finally { + setState(() => isLoading = false); + } + }, + onBiometricAuth: () async { + setState(() => isLoading = true); + try { + final authenticated = await authenticateWithBiometrics(); + if (authenticated) { + await processPaymentWithBiometrics(); + Navigator.of(context).pop(); + } + } catch (e) { + showErrorDialog(e.toString()); + } finally { + setState(() => isLoading = false); + } + }, +); +``` + +## Parameters + +### PaymentOverlay.show() + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `context` | `BuildContext` | ✅ | The build context for showing the overlay | +| `order` | `SnWalletOrder` | ✅ | The order to be paid | +| `onPaymentSuccess` | `Function(SnWalletOrder)?` | ❌ | Callback when payment succeeds with completed order | +| `onPaymentError` | `Function(String)?` | ❌ | Callback when payment fails with error message | +| `onCancel` | `VoidCallback?` | ❌ | Callback when payment is cancelled | +| `enableBiometric` | `bool` | ❌ | Whether to show biometric option (default: true) | + +## API Integration + +The PaymentOverlay automatically handles payment processing by calling the `/orders/{orderId}/pay` endpoint with the following request body: + +### PIN Payment +```json +{ + "pin": "123456" +} +``` + +### Biometric Payment +```json +{ + "biometric": true +} +``` + +### Response +The API should return the completed `SnWalletOrder` object: + +```json +{ + "id": "order_123", + "amount": 2500, + "currency": "USD", + "description": "Premium Subscription", + "status": "completed", + "processorReference": "txn_abc123", + // ... other order fields +} +``` + +### Error Handling +The widget handles common HTTP status codes: +- `401`: Invalid PIN or biometric authentication failed +- `400`: Bad request with custom error message +- Other errors: Generic payment failed message + +### Implementation Example + +```dart +import 'package:local_auth/local_auth.dart'; + +class BiometricService { + final LocalAuthentication _auth = LocalAuthentication(); + + Future isAvailable() async { + final isAvailable = await _auth.canCheckBiometrics; + final isDeviceSupported = await _auth.isDeviceSupported(); + return isAvailable && isDeviceSupported; + } + + Future authenticate() async { + try { + final bool didAuthenticate = await _auth.authenticate( + localizedReason: 'Please authenticate to complete payment', + options: const AuthenticationOptions( + biometricOnly: true, + stickyAuth: true, + ), + ); + return didAuthenticate; + } catch (e) { + print('Biometric authentication error: $e'); + return false; + } + } +} +``` + +## Localization + +Add these keys to your localization files: + +```json +{ + "paymentVerification": "Payment Verification", + "paymentSummary": "Payment Summary", + "amount": "Amount", + "description": "Description", + "pinCode": "PIN Code", + "biometric": "Biometric", + "enterPinToConfirm": "Enter your 6-digit PIN to confirm payment", + "clearPin": "Clear PIN", + "useBiometricToConfirm": "Use biometric authentication to confirm payment", + "touchSensorToAuthenticate": "Touch the sensor to authenticate", + "authenticating": "Authenticating...", + "authenticateNow": "Authenticate Now", + "confirm": "Confirm", + "cancel": "Cancel", + "paymentFailed": "Payment failed. Please try again.", + "invalidPin": "Invalid PIN. Please try again.", + "biometricAuthFailed": "Biometric authentication failed. Please try again.", + "paymentSuccess": "Payment completed successfully!", + "paymentError": "Payment failed: {error}" +} +``` + +## Styling + +The widget automatically adapts to your app's theme. It uses: + +- `Theme.of(context).colorScheme.primary` for primary elements +- `Theme.of(context).colorScheme.surface` for backgrounds +- `Theme.of(context).textTheme` for typography + +## Security Considerations + +1. **PIN Handling**: The PIN is passed as a string to your callback. Ensure you handle it securely and don't log it. +2. **Biometric Authentication**: Always verify biometric authentication on your backend. +3. **Network Security**: Use HTTPS for all payment-related API calls. +4. **Data Validation**: Validate all payment data on your backend before processing. + +## Example Integration + +See `payment_overlay_example.dart` for a complete working example that demonstrates: + +- How to show the overlay +- Handling PIN and biometric authentication +- Processing payments +- Error handling +- Loading states + +## Dependencies + +- `flutter/material.dart` - Material Design components +- `flutter/services.dart` - Input formatters and system services +- `flutter_riverpod/flutter_riverpod.dart` - State management and dependency injection +- `gap/gap.dart` - Spacing widgets +- `material_symbols_icons/symbols.dart` - Material Symbols icons +- `easy_localization/easy_localization.dart` - Internationalization +- `dio/dio.dart` - HTTP client for API calls +- `solian/models/wallet.dart` - Wallet order model +- `solian/widgets/common/sheet_scaffold.dart` - Sheet scaffold widget +- `solian/pods/network.dart` - API client provider \ No newline at end of file diff --git a/lib/widgets/payment/payment_overlay.dart b/lib/widgets/payment/payment_overlay.dart new file mode 100644 index 0000000..b6648c5 --- /dev/null +++ b/lib/widgets/payment/payment_overlay.dart @@ -0,0 +1,495 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:island/models/wallet.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:island/pods/network.dart'; +import 'package:dio/dio.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter/services.dart'; + +class PaymentOverlay extends HookConsumerWidget { + final SnWalletOrder order; + final Function(SnWalletOrder completedOrder)? onPaymentSuccess; + final Function(String error)? onPaymentError; + final VoidCallback? onCancel; + final bool enableBiometric; + + const PaymentOverlay({ + super.key, + required this.order, + this.onPaymentSuccess, + this.onPaymentError, + this.onCancel, + this.enableBiometric = true, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SheetScaffold( + titleText: 'Solarpay', + heightFactor: 0.7, + child: _PaymentContent( + order: order, + onPaymentSuccess: onPaymentSuccess, + onPaymentError: onPaymentError, + onCancel: onCancel, + enableBiometric: enableBiometric, + ), + ), + ), + ); + } + + static Future show({ + required BuildContext context, + required SnWalletOrder order, + bool enableBiometric = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + useSafeArea: true, + builder: + (context) => PaymentOverlay( + order: order, + enableBiometric: enableBiometric, + onPaymentSuccess: (completedOrder) { + Navigator.of(context).pop(completedOrder); + }, + onPaymentError: (err) { + Navigator.of(context).pop(); + showErrorAlert(err); + }, + onCancel: () { + Navigator.of(context).pop(); + }, + ), + ); + } +} + +class _PaymentContent extends ConsumerStatefulWidget { + final SnWalletOrder order; + final Function(SnWalletOrder)? onPaymentSuccess; + final Function(String)? onPaymentError; + final VoidCallback? onCancel; + final bool enableBiometric; + + const _PaymentContent({ + required this.order, + this.onPaymentSuccess, + this.onPaymentError, + this.onCancel, + this.enableBiometric = true, + }); + + @override + ConsumerState<_PaymentContent> createState() => _PaymentContentState(); +} + +class _PaymentContentState extends ConsumerState<_PaymentContent> { + static const String _pinStorageKey = 'app_pin_code'; + static final _secureStorage = FlutterSecureStorage(); + + final LocalAuthentication _localAuth = LocalAuthentication(); + + String _pin = ''; + bool _isPinMode = true; + bool _hasBiometricSupport = false; + bool _hasStoredPin = false; + + @override + void initState() { + super.initState(); + _initializeBiometric(); + } + + @override + void dispose() { + super.dispose(); + } + + Future _initializeBiometric() async { + try { + // Check if biometric is available + final isAvailable = await _localAuth.isDeviceSupported(); + final canCheckBiometrics = await _localAuth.canCheckBiometrics; + _hasBiometricSupport = isAvailable && canCheckBiometrics; + + // Check if PIN is stored + final storedPin = await _secureStorage.read(key: _pinStorageKey); + _hasStoredPin = storedPin != null && storedPin.isNotEmpty; + + // Set initial mode based on stored PIN and biometric support + if (_hasStoredPin && _hasBiometricSupport && widget.enableBiometric) { + _isPinMode = false; + // Automatically trigger biometric authentication + WidgetsBinding.instance.addPostFrameCallback((_) { + _authenticateWithBiometric(); + }); + } else { + _isPinMode = true; + } + + if (mounted) { + setState(() {}); + } + } catch (e) { + // Fallback to PIN mode if biometric setup fails + _isPinMode = true; + if (mounted) { + setState(() {}); + } + } + } + + void _onPinSubmit(String pin) { + _pin = pin; + if (pin.length == 6) { + _processPaymentWithPin(pin); + } + } + + Future _processPaymentWithPin(String pin) async { + showLoadingModal(context); + + try { + // Store PIN securely for future biometric authentication + if (_hasBiometricSupport && widget.enableBiometric && !_hasStoredPin) { + await _secureStorage.write(key: _pinStorageKey, value: pin); + _hasStoredPin = true; + } + + await _makePaymentRequest(pin); + } catch (err) { + widget.onPaymentError?.call(err.toString()); + _pin = ''; + } finally { + if (mounted) { + hideLoadingModal(context); + } + } + } + + Future _authenticateWithBiometric() async { + showLoadingModal(context); + + try { + // Perform biometric authentication + final bool didAuthenticate = await _localAuth.authenticate( + localizedReason: 'biometricPrompt'.tr(), + options: const AuthenticationOptions( + biometricOnly: true, + stickyAuth: true, + ), + ); + + if (didAuthenticate) { + // Retrieve stored PIN and process payment + final storedPin = await _secureStorage.read(key: _pinStorageKey); + if (storedPin != null && storedPin.isNotEmpty) { + await _makePaymentRequest(storedPin); + } else { + // Fallback to PIN mode if no stored PIN + _fallbackToPinMode('noStoredPin'.tr()); + } + } else { + // Biometric authentication failed, fallback to PIN mode + _fallbackToPinMode('biometricAuthFailed'.tr()); + } + } catch (err) { + // Handle biometric authentication errors + String errorMessage = 'biometricAuthFailed'.tr(); + if (err is PlatformException) { + switch (err.code) { + case 'NotAvailable': + errorMessage = 'biometricNotAvailable'.tr(); + break; + case 'NotEnrolled': + errorMessage = 'biometricNotEnrolled'.tr(); + break; + case 'LockedOut': + case 'PermanentlyLockedOut': + errorMessage = 'biometricLockedOut'.tr(); + break; + default: + errorMessage = 'biometricAuthFailed'.tr(); + } + } + _fallbackToPinMode(errorMessage); + } finally { + if (mounted) { + hideLoadingModal(context); + } + } + } + + /// Unified method for making payment requests with PIN + Future _makePaymentRequest(String pin) async { + try { + final client = ref.read(apiClientProvider); + final response = await client.post( + '/orders/${widget.order.id}/pay', + data: {'pin_code': pin}, + ); + + final completedOrder = SnWalletOrder.fromJson(response.data); + widget.onPaymentSuccess?.call(completedOrder); + } catch (err) { + String errorMessage = 'paymentFailed'.tr(); + if (err is DioException) { + if (err.response?.statusCode == 403 || + err.response?.statusCode == 401) { + // PIN is invalid + errorMessage = 'invalidPin'.tr(); + // If this was a biometric attempt with stored PIN, remove the stored PIN + if (!_isPinMode) { + await _secureStorage.delete(key: _pinStorageKey); + _hasStoredPin = false; + _fallbackToPinMode(errorMessage); + return; + } + } else if (err.response?.statusCode == 400) { + errorMessage = err.response?.data?['error'] ?? errorMessage; + } + } + throw errorMessage; + } + } + + void _fallbackToPinMode(String? message) { + setState(() { + _isPinMode = true; + }); + if (message != null && message.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + + String _formatCurrency(int amount, String currency) { + final value = amount / 100.0; + return '${value.toStringAsFixed(2)} $currency'; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Order Summary + _buildOrderSummary(), + const Gap(32), + + // Authentication Content + Expanded( + child: _isPinMode ? _buildPinInput() : _buildBiometricAuth(), + ), + + // Action Buttons + const Gap(24), + _buildActionButtons(), + ], + ), + ); + } + + Widget _buildOrderSummary() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.receipt, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Text( + 'paymentSummary'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + const Gap(12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'amount'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + _formatCurrency(widget.order.amount, widget.order.currency), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + if (widget.order.remarks != null) ...[ + const Gap(8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'description'.tr(), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const Spacer(), + Expanded( + flex: 2, + child: Text( + widget.order.remarks!, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.end, + ), + ), + ], + ), + ], + ], + ), + ); + } + + Widget _buildPinInput() { + return Column( + children: [ + Text( + 'enterPinToConfirm'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + const Gap(24), + OtpTextField( + numberOfFields: 6, + borderColor: Theme.of(context).colorScheme.outline, + focusedBorderColor: Theme.of(context).colorScheme.primary, + showFieldAsBox: true, + obscureText: true, + keyboardType: TextInputType.number, + fieldWidth: 48, + fieldHeight: 56, + borderRadius: BorderRadius.circular(8), + borderWidth: 1, + textStyle: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600), + onSubmit: _onPinSubmit, + onCodeChanged: (String code) { + _pin = code; + setState(() {}); + }, + ), + ], + ); + } + + Widget _buildBiometricAuth() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Symbols.fingerprint, + size: 64, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + const Gap(24), + Text( + 'useBiometricToConfirm'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + const Gap(16), + Text( + 'touchSensorToAuthenticate'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const Gap(32), + ElevatedButton.icon( + onPressed: _authenticateWithBiometric, + icon: const Icon(Symbols.fingerprint), + label: Text('authenticateNow'.tr()), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + const Gap(16), + TextButton( + onPressed: () => _fallbackToPinMode(null), + child: Text('usePinInstead'.tr()), + ), + ], + ); + } + + Widget _buildActionButtons() { + return Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: widget.onCancel, + child: Text('cancel'.tr()), + ), + ), + if (_isPinMode && _pin.length == 6) ...[ + const Gap(12), + Expanded( + child: ElevatedButton( + onPressed: () => _processPaymentWithPin(_pin), + child: Text('confirm'.tr()), + ), + ), + ], + ], + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f56efee..b15f083 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin"); flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin"); + flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar); g_autoptr(FlPluginRegistrar) flutter_timezone_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin"); flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6f569e8..7a0cc67 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux file_selector_linux flutter_platform_alert + flutter_secure_storage flutter_timezone flutter_udid flutter_webrtc diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8824bf0..191be60 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -20,6 +20,7 @@ import flutter_webrtc import gal import irondash_engine_context import livekit_client +import local_auth_darwin import media_kit_libs_macos_video import media_kit_video import package_info_plus @@ -51,6 +52,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) + FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 0f01142..1b2b2dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: build - sha256: "7cf79af8eb6023bee797a77b067fb6e63ac5650f3789546e023958098feb776e" + sha256: "74273591bd8b7f82eeb1f191c1b65a6576535bbfd5ca3722778b07d5702d33cc" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" build_config: dependency: transitive description: @@ -173,26 +173,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "7a507e6026abe52074836d51a945bfad456daa7493eb7a6cac565e490e7d5b54" + sha256: badce70566085f2e87434531c4a6bc8e833672f755fc51146d612245947e91c9 url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1ce1e5063b564f26c27bda54c82a3d38339df69ec58f90e0017f447de77e4839" + sha256: b9070a4127033777c0e63195f6f117ed16a351ed676f6313b095cf4f328c0b82 url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "564230f3fd9363df7870058fef11ec5502ee620aec3b1ee8106b943be5c63a76" + sha256: "1cdfece3eeb3f1263f7dbf5bcc0cba697bd0c22d2c866cb4b578c954dbb09bcf" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" built_collection: dependency: transitive description: @@ -891,6 +891,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6" + url: "https://pub.dev" + source: hosted + version: "4.2.1" flutter_svg: dependency: "direct main" description: @@ -1245,6 +1253,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.8" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" + url: "https://pub.dev" + source: hosted + version: "1.0.49" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" + url: "https://pub.dev" + source: hosted + version: "1.4.3" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ad33e21..e2946ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -116,6 +116,8 @@ dependencies: sign_in_with_apple: ^7.0.1 flutter_svg: ^2.1.0 native_exif: ^0.6.2 + local_auth: ^2.3.0 + flutter_secure_storage: ^4.2.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0ae8b18..d6d49aa 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); LiveKitPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LiveKitPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1c7bca6..93d5067 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gal irondash_engine_context livekit_client + local_auth_windows media_kit_libs_windows_video media_kit_video pasteboard