feat: legacy mobile apps created

This commit is contained in:
Achintha Isuru
2025-12-02 23:51:04 -05:00
parent 850441ca64
commit 8e7753b324
1519 changed files with 0 additions and 16 deletions

View File

@@ -0,0 +1,41 @@
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/data/models/staff/full_address_model.dart';
import 'package:krow/features/profile/address/data/gql.dart';
@injectable
class AddressApiProvider {
final ApiClient _apiClient;
AddressApiProvider(this._apiClient);
Stream<FullAddress?> getStaffAddress() async* {
await for (var response
in _apiClient.queryWithCache(schema: staffAddress)) {
if (response == null) {
continue;
}
if (response.hasException) {
throw Exception(response.exception.toString());
}
final address =
FullAddress.fromJson(response.data?['me']?['full_address'] ?? {});
yield address;
}
}
Future<void> putAddress(FullAddress address) async {
final Map<String, dynamic> variables = {
'input': address.toJson(),
};
var result =
await _apiClient.mutate(schema: saveFullAddress, body: variables);
if (result.hasException) {
throw Exception(result.exception.toString());
}
}
}

View File

@@ -0,0 +1,7 @@
import 'package:krow/core/data/models/staff/full_address_model.dart';
abstract class AddressRepository {
Stream<FullAddress?> getStaffAddress();
Future<void> putAddress(FullAddress address);
}

View File

@@ -0,0 +1,26 @@
const String staffAddress = r'''
query staffAddress {
me {
id
full_address {
street_number
zip_code
latitude
longitude
formatted_address
street
region
city
country
}
}
}
''';
const String saveFullAddress = r'''
mutation saveFullAddress($input: AddressInput!) {
update_staff_address(input: $input) {
}
}
''';

View File

@@ -0,0 +1,22 @@
import 'package:injectable/injectable.dart';
import 'package:krow/core/data/models/staff/full_address_model.dart';
import 'package:krow/features/profile/address/data/address_api_provider.dart';
import 'package:krow/features/profile/address/data/address_repository.dart';
@Singleton(as: AddressRepository)
class AddressRepositoryImpl implements AddressRepository {
final AddressApiProvider _apiProvider;
AddressRepositoryImpl({required AddressApiProvider apiProvider})
: _apiProvider = apiProvider;
@override
Stream<FullAddress?> getStaffAddress() {
return _apiProvider.getStaffAddress();
}
@override
Future<void> putAddress(FullAddress address) {
return _apiProvider.putAddress(address);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/models/staff/full_address_model.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/features/profile/address/data/address_repository.dart';
import 'package:krow/features/profile/address/domain/google_places_service.dart';
part 'address_event.dart';
part 'address_state.dart';
class AddressBloc extends Bloc<AddressEvent, AddressState> {
AddressBloc() : super(const AddressState()) {
on<InitializeAddressEvent>(_onInitialize);
on<SubmitAddressEvent>(_onSubmit);
on<AddressQueryChangedEvent>(_onQueryChanged);
on<AddressSelectEvent>(_onSelect);
}
void _onInitialize(
InitializeAddressEvent event, Emitter<AddressState> emit) async {
emit(state.copyWith(status: StateStatus.loading));
await for (var address in getIt<AddressRepository>().getStaffAddress()) {
emit(state.copyWith(
fullAddress: address,
status: StateStatus.idle,
));
}
}
void _onQueryChanged(
AddressQueryChangedEvent event, Emitter<AddressState> emit) async {
try {
final googlePlacesService = GooglePlacesService();
final suggestions =
await googlePlacesService.fetchSuggestions(event.query);
emit(state.copyWith(suggestions: suggestions));
} catch (e) {
if (kDebugMode) print(e);
}
}
void _onSelect(AddressSelectEvent event, Emitter<AddressState> emit) async {
final googlePlacesService = GooglePlacesService();
final fullAddress =
await googlePlacesService.getPlaceDetails(event.place.placeId);
FullAddress address = FullAddress.fromGoogle(fullAddress);
emit(state.copyWith(suggestions: [], fullAddress: address));
}
void _onSubmit(SubmitAddressEvent event, Emitter<AddressState> emit) async {
emit(state.copyWith(status: StateStatus.loading));
try {
await getIt<AddressRepository>().putAddress(state.fullAddress!);
emit(state.copyWith(status: StateStatus.success));
} catch (e) {
emit(state.copyWith(status: StateStatus.error));
}
}
}

View File

@@ -0,0 +1,24 @@
part of 'address_bloc.dart';
@immutable
sealed class AddressEvent {}
class InitializeAddressEvent extends AddressEvent {
InitializeAddressEvent();
}
class AddressQueryChangedEvent extends AddressEvent {
final String query;
AddressQueryChangedEvent(this.query);
}
class SubmitAddressEvent extends AddressEvent {
SubmitAddressEvent();
}
class AddressSelectEvent extends AddressEvent {
final MapPlace place;
AddressSelectEvent(this.place);
}

View File

@@ -0,0 +1,26 @@
part of 'address_bloc.dart';
@immutable
class AddressState {
final StateStatus status;
final FullAddress? fullAddress;
final List<MapPlace> suggestions;
const AddressState({
this.status = StateStatus.idle,
this.fullAddress,
this.suggestions = const [],
});
AddressState copyWith({
StateStatus? status,
FullAddress? fullAddress,
List<MapPlace>? suggestions,
}) {
return AddressState(
status: status ?? this.status,
suggestions: suggestions ?? this.suggestions,
fullAddress: fullAddress ?? this.fullAddress,
);
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:injectable/injectable.dart';
@singleton
class GooglePlacesService {
Future<List<MapPlace>> fetchSuggestions(String query) async {
final String apiKey = dotenv.env['GOOGLE_MAP']!;
const String baseUrl =
'https://maps.googleapis.com/maps/api/place/autocomplete/json';
final Uri uri =
Uri.parse('$baseUrl?input=$query&key=$apiKey&types=geocode');
final response = await http.get(uri);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> predictions = data['predictions'];
return predictions.map((prediction) {
return MapPlace.fromJson(prediction);
}).toList();
} else {
throw Exception('Failed to fetch place suggestions');
}
}
Future<Map<String, dynamic>> getPlaceDetails(String placeId) async {
final String apiKey = dotenv.env['GOOGLE_MAP']!;
final String url =
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
final result = data['result'];
final location = result['geometry']['location'];
Map<String, dynamic> addressDetails = {
'lat': location['lat'], // Latitude
'lng': location['lng'], // Longitude
'formatted_address': result['formatted_address'], // Full Address
};
for (var component in result['address_components']) {
List types = component['types'];
if (types.contains('street_number')) {
addressDetails['street_number'] = component['long_name'];
}
if (types.contains('route')) {
addressDetails['street'] = component['long_name'];
}
if (types.contains('locality')) {
addressDetails['city'] = component['long_name'];
}
if (types.contains('administrative_area_level_1')) {
addressDetails['state'] = component['long_name'];
}
if (types.contains('country')) {
addressDetails['country'] = component['long_name'];
}
if (types.contains('postal_code')) {
addressDetails['postal_code'] = component['long_name'];
}
}
return addressDetails;
} else {
throw Exception('Failed to fetch place details');
}
}
}
class MapPlace {
final String description;
final String placeId;
MapPlace({required this.description, required this.placeId});
toJson() {
return {
'description': description,
'place_id': placeId,
};
}
factory MapPlace.fromJson(Map<String, dynamic> json) {
return MapPlace(
description: json['description'],
placeId: json['place_id'],
);
}
}

View File

@@ -0,0 +1,150 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_suggestion_input.dart';
import 'package:krow/features/profile/address/domain/bloc/address_bloc.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
@RoutePage()
class AddressScreen extends StatefulWidget implements AutoRouteWrapper {
final bool isInEditMode;
const AddressScreen({super.key, this.isInEditMode = true});
@override
State<AddressScreen> createState() => _AddressScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider(
create: (context) => AddressBloc()..add(InitializeAddressEvent()),
child: this,
);
}
}
class _AddressScreenState extends State<AddressScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
showNotification: widget.isInEditMode,
titleText: 'location_and_availability'.tr(),
),
body: BlocConsumer<AddressBloc, AddressState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == StateStatus.success) {
if (widget.isInEditMode) {
Navigator.pop(context);
} else {
context.router.push(
WorkingAreaRoute(),
);
}
}
},
builder: (context, state) {
return ModalProgressHUD(
inAsyncCall: state.status == StateStatus.loading,
child: ScrollLayoutHelper(
padding: const EdgeInsets.all(16),
upperWidget: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!widget.isInEditMode) ...[
const Gap(4),
Text(
'what_is_your_address'.tr(),
style: AppTextStyles.headingH1,
),
Text(
'let_us_know_your_home_base'.tr(),
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
),
const Gap(24),
]
else
const Gap(8),
KwSuggestionInput(
title: 'address'.tr(),
hintText: 'select_address'.tr(),
horizontalPadding: 16,
items: state.suggestions,
onQueryChanged: (query) {
context
.read<AddressBloc>()
.add(AddressQueryChangedEvent(query));
},
itemToStringBuilder: (item) => item.description,
onSelected: (item) {
context.read<AddressBloc>().add(AddressSelectEvent(item));
},
),
const Gap(8),
Container(
padding: const EdgeInsets.all(12),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
..._textBlock('country'.tr(), state.fullAddress?.country),
..._textBlock('state'.tr(), state.fullAddress?.region),
..._textBlock('city'.tr(), state.fullAddress?.city),
..._textBlock('apt_suite_building'.tr(),
state.fullAddress?.streetNumber),
..._textBlock(
'street_address'.tr(), state.fullAddress?.street),
..._textBlock('zip_code'.tr(), state.fullAddress?.zipCode,
hasNext: false),
]),
)
],
),
lowerWidget: KwButton.primary(
disabled: state.fullAddress == null,
label: widget.isInEditMode
? 'save_changes'.tr()
: 'save_and_continue'.tr(),
onPressed: () {
if (widget.isInEditMode) {
context.read<AddressBloc>().add(SubmitAddressEvent());
} else {
context.router.push(WorkingAreaRoute(isInEditMode: false));
}
}),
),
);
},
),
);
}
List<Widget> _textBlock(String key, String? value, {bool hasNext = true}) {
return [
...[
Text(
key,
style: AppTextStyles.captionReg.copyWith(color: AppColors.blackGray),
),
const Gap(8),
Text(
value ?? '',
style: AppTextStyles.bodyMediumMed,
),
hasNext ? const Gap(24) : const Gap(0),
],
];
}
}