Skip to content
This repository has been archived by the owner on Aug 26, 2023. It is now read-only.

Commit

Permalink
Add custom API key settings section (#195)
Browse files Browse the repository at this point in the history
* Add API Key Settings Section

* Implement Reset API Key in clima_data

* Fixed an embarrasing mistake

* Remove unrelated changes

* Add `ApiKeyModel`

* [data] use snake case for api key prefs key

It's more consistent with other prefs keys in Clima.

* Delete loading_screen.dart

* Fix some errors

* Fix some more errors

* Some refactoring

* Only use double quotes when needed

* Fix formatting issues

* Fix formatting issues

* Add `ApiKeyStateNotifier` class, and use it in `ApiKeyDialog`

* Show error when API key is invalid

* Use correct response code for invalid API key error

* Add dynamic subtitle for the API key settings tile

* Show snackbar when API key was updated successfully

* Set larger `errorMaxLines` for API key text field

* Update packages/clima_ui/lib/screens/settings_screen.dart

Co-authored-by: Mohammed Anas <[email protected]>

* Add comment suggestions

* Update packages/clima_ui/lib/screens/weather_screen.dart

Co-authored-by: Mohammed Anas <[email protected]>

* Refactor showFailureSnackBar to use the showSnackBar function

* Fixed an error where errorMaxLines property was placed outside the InputDecoration widget

* Remove unused final

* Organize imports

* Add clarification and TODO comment

* Clarify info about shared API key

* Re-add empty line

Why was it removed anyway?

Co-authored-by: mhmdanas <[email protected]>
  • Loading branch information
prestosole and triallax authored Feb 9, 2022
1 parent a652d03 commit 8a1d94b
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 46 deletions.
39 changes: 11 additions & 28 deletions packages/clima_data/lib/data_sources/api_key_local_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,29 @@

import 'package:clima_core/either.dart';
import 'package:clima_core/failure.dart';
import 'package:clima_data/models/api_key_model.dart';
import 'package:clima_data/providers.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod/riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

const _apiKeyPrefsKey = 'openWeatherMapApiKey';
const _apiKeyPrefsKey = 'open_weather_map_api_key';

class ApiKeyLocalDataSource {
ApiKeyLocalDataSource(this._prefs);

final SharedPreferences _prefs;

Future<Either<Failure, String?>> getApiKey() async =>
Right(_prefs.getString(_apiKeyPrefsKey));
Future<Either<Failure, ApiKeyModel>> getApiKey() async =>
Right(ApiKeyModel.parse(_prefs.getString(_apiKeyPrefsKey)));

Future<Either<Failure, void>> setApiKey(String apiKey) async {
final response = await http.get(
Uri(
scheme: 'https',
host: 'api.openweathermap.org',
path: '/data/2.5/weather',
queryParameters: {'appid': apiKey},
),
);

switch (response.statusCode) {
case 400:
_prefs.setString(_apiKeyPrefsKey, apiKey);

return const Right(null);

case 404:
return const Left(InvalidApiKey());

case 503:
return const Left(ServerDown());

default:
return const Left(FailedToParseResponse());
Future<Either<Failure, void>> setApiKey(ApiKeyModel model) async {
if (model.isCustom) {
await _prefs.setString(_apiKeyPrefsKey, model.apiKey);
} else {
await _prefs.remove(_apiKeyPrefsKey);
}

return const Right(null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FullWeatherRemoteDataSource {
final GeocodingRepo _geocodingRepo;

Future<Either<Failure, FullWeatherModel>> getFullWeather(City city) async {
final apiKey = (await _apiKeyRepo.getApiKey()).fold((_) => null, id)!;
final apiKeyModel = (await _apiKeyRepo.getApiKey()).fold((_) => null, id)!;

final coordinates =
(await _geocodingRepo.getCoordinates(city)).fold((_) => null, id)!;
Expand All @@ -39,7 +39,7 @@ class FullWeatherRemoteDataSource {
queryParameters: {
'lon': coordinates.long.toString(),
'lat': coordinates.lat.toString(),
'appid': apiKey,
'appid': apiKeyModel.apiKey,
'units': 'metric',
'exclude': 'minutely,alerts',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class GeocodingRemoteDataSource {
Future<Either<Failure, GeographicCoordinatesModel>> getCoordinates(
City city,
) async {
final apiKey = (await _apiKeyRepo.getApiKey()).fold((_) => null, id)!;
final apiKeyModel = (await _apiKeyRepo.getApiKey()).fold((_) => null, id)!;

final response = await http.get(
Uri(
Expand All @@ -32,7 +32,7 @@ class GeocodingRemoteDataSource {
path: '/geo/1.0/direct',
queryParameters: {
'q': city.name,
'appid': apiKey,
'appid': apiKeyModel.apiKey,
'limit': '1',
},
),
Expand Down
32 changes: 32 additions & 0 deletions packages/clima_data/lib/models/api_key_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import 'package:equatable/equatable.dart';

const _defaultApiKey = '0cca00b6155fcac417cc140a5deba9a4';

class ApiKeyModel extends Equatable {
// There's an underscore here because `default` can't be an identifier in
// Dart.
const ApiKeyModel.default_()
: apiKey = _defaultApiKey,
isCustom = false;

const ApiKeyModel.custom(this.apiKey) : isCustom = true;

factory ApiKeyModel.parse(String? string) {
if (string == null) return const ApiKeyModel.default_();

return ApiKeyModel.custom(string);
}

final String apiKey;

final bool isCustom;

@override
List<Object?> get props => [apiKey, isCustom];
}
19 changes: 11 additions & 8 deletions packages/clima_data/lib/repos/api_key_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,37 @@ import 'dart:async';
import 'package:clima_core/either.dart';
import 'package:clima_core/failure.dart';
import 'package:clima_data/data_sources/api_key_local_data_source.dart';
import 'package:clima_data/models/api_key_model.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod/riverpod.dart';

const _defaultApiKey = '0cca00b6155fcac417cc140a5deba9a4';

class ApiKeyRepo {
ApiKeyRepo(this.localDataSource);

final ApiKeyLocalDataSource localDataSource;

Future<Either<Failure, String>> getApiKey() async =>
(await localDataSource.getApiKey()).map((key) => key ?? _defaultApiKey);
Future<Either<Failure, ApiKeyModel>> getApiKey() =>
localDataSource.getApiKey();

Future<Either<Failure, void>> setApiKey(ApiKeyModel apiKeyModel) async {
if (!apiKeyModel.isCustom) {
return localDataSource.setApiKey(apiKeyModel);
}

Future<Either<Failure, void>> setApiKey(String apiKey) async {
final response = await http.get(
Uri(
scheme: 'https',
host: 'api.openweathermap.org',
path: '/data/2.5/weather',
queryParameters: {'appid': apiKey},
queryParameters: {'appid': apiKeyModel.apiKey},
),
);

switch (response.statusCode) {
case 400:
return localDataSource.setApiKey(apiKey);
return localDataSource.setApiKey(apiKeyModel);

case 404:
case 401:
return const Left(InvalidApiKey());

case 503:
Expand Down
52 changes: 51 additions & 1 deletion packages/clima_ui/lib/screens/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ import 'package:clima_data/models/dark_theme_model.dart';
import 'package:clima_data/models/theme_model.dart';
import 'package:clima_domain/entities/unit_system.dart';
import 'package:clima_ui/screens/about_screen.dart';
import 'package:clima_ui/state_notifiers/api_key_state_notifier.dart' as a;
import 'package:clima_ui/state_notifiers/theme_state_notifier.dart';
import 'package:clima_ui/state_notifiers/unit_system_state_notifier.dart'
hide Error;
import 'package:clima_ui/widgets/dialogs/api_key/api_key_dialog.dart';
import 'package:clima_ui/widgets/dialogs/api_key/api_key_info_dialog.dart';
import 'package:clima_ui/widgets/dialogs/api_key/api_key_reset_dialog.dart';
import 'package:clima_ui/widgets/dialogs/dark_theme_dialog.dart';
import 'package:clima_ui/widgets/dialogs/theme_dialog.dart';
import 'package:clima_ui/widgets/dialogs/unit_system_dialog.dart';
import 'package:clima_ui/widgets/settings/settings_divider.dart';
import 'package:clima_ui/widgets/settings/settings_header.dart';
import 'package:clima_ui/widgets/settings/settings_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class SettingScreen extends ConsumerWidget {
@override
Expand All @@ -28,6 +32,8 @@ class SettingScreen extends ConsumerWidget {
final darkTheme = ref.watch(
themeStateNotifierProvider.select((state) => state.darkTheme),
);
final apiKey = ref
.watch(a.apiKeyStateNotifierProvider.select((state) => state.apiKey!));

final unitSystem = ref.watch(
unitSystemStateNotifierProvider.select((state) => state.unitSystem!),
Expand Down Expand Up @@ -125,6 +131,50 @@ class SettingScreen extends ConsumerWidget {
),
),
const SettingsDivider(),
const SettingsHeader(title: 'API key'),
SettingsTile(
title: 'API key',
subtitle: apiKey.isCustom
? 'Currently using custom API key'
: 'Currently using default API key (not recommended)',
leading: Icon(
Icons.keyboard_outlined,
color: Theme.of(context).iconTheme.color,
),
onTap: () async {
await showDialog(
context: context,
builder: (context) => const ApiKeyDialog(),
);
},
),
SettingsTile(
title: 'Reset API key',
leading: Icon(
Icons.restore_outlined,
color: Theme.of(context).iconTheme.color,
),
onTap: () {
showDialog(
context: context,
builder: (context) => const ApiKeyResetDialog(),
);
},
),
SettingsTile(
title: 'Learn more',
leading: Icon(
Icons.launch_outlined,
color: Theme.of(context).iconTheme.color,
),
onTap: () {
showDialog(
context: context,
builder: (context) => const ApiKeyInfoDialog(),
);
},
),
const SettingsDivider(),
const SettingsHeader(
title: 'About',
),
Expand Down
9 changes: 7 additions & 2 deletions packages/clima_ui/lib/screens/weather_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

import 'package:clima_domain/entities/city.dart';
import 'package:clima_domain/entities/unit_system.dart';
import 'package:clima_ui/state_notifiers/api_key_state_notifier.dart' as a;
import 'package:clima_ui/state_notifiers/city_state_notifier.dart' as c;
import 'package:clima_ui/state_notifiers/full_weather_state_notifier.dart' as w;
import 'package:clima_ui/state_notifiers/unit_system_state_notifier.dart' as u;
import 'package:clima_ui/utilities/constants.dart';
import 'package:clima_ui/utilities/failure_snack_bar.dart';
import 'package:clima_ui/utilities/hooks.dart';
import 'package:clima_ui/utilities/snack_bars.dart';
import 'package:clima_ui/widgets/others/failure_banner.dart';
import 'package:clima_ui/widgets/others/overflow_menu_button.dart';
import 'package:clima_ui/widgets/weather/additional_info_widget.dart';
Expand All @@ -36,6 +37,9 @@ class WeatherScreen extends HookConsumerWidget {
final fullWeatherStateNotifier =
ref.watch(w.fullWeatherStateNotifierProvider.notifier);

final apiKeyStateNotifier =
ref.watch(a.apiKeyStateNotifierProvider.notifier);

final controller = useFloatingSearchBarController();

final cityStateNotifier = ref.watch(c.cityStateNotifierProvider.notifier);
Expand All @@ -54,14 +58,15 @@ class WeatherScreen extends HookConsumerWidget {
() {
Future.microtask(
() => Future.wait([
apiKeyStateNotifier.loadApiKey(),
unitSystemStateNotifier.loadUnitSystem(),
fullWeatherStateNotifier.loadFullWeather(),
]),
);

return null;
},
[fullWeatherStateNotifier, unitSystemStateNotifier],
[fullWeatherStateNotifier, unitSystemStateNotifier, apiKeyStateNotifier],
);

useEffect(
Expand Down
78 changes: 78 additions & 0 deletions packages/clima_ui/lib/state_notifiers/api_key_state_notifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import 'package:clima_core/failure.dart';
import 'package:clima_data/models/api_key_model.dart';
import 'package:clima_data/repos/api_key_repo.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:riverpod/riverpod.dart';

@sealed
@immutable
abstract class ApiKeyState extends Equatable {
const ApiKeyState();

ApiKeyModel? get apiKey => null;

@override
List<Object?> get props => const [];
}

class Empty extends ApiKeyState {
const Empty();
}

class Loading extends ApiKeyState {
const Loading();
}

class Loaded extends ApiKeyState {
const Loaded(this.apiKey);

@override
final ApiKeyModel apiKey;

@override
List<Object?> get props => [apiKey];
}

class Error extends ApiKeyState {
const Error(this.failure, {this.apiKey});

final Failure failure;

@override
final ApiKeyModel? apiKey;

@override
List<Object?> get props => [failure, apiKey];
}

class ApiKeyStateNotifier extends StateNotifier<ApiKeyState> {
ApiKeyStateNotifier(this._apiKeyRepo) : super(const Empty());

final ApiKeyRepo _apiKeyRepo;

Future<void> loadApiKey() async {
state = const Loading();
final data = await _apiKeyRepo.getApiKey();
state = data.fold((failure) => Error(failure), (city) => Loaded(city));
}

Future<void> setApiKey(ApiKeyModel apiKey) async {
(await _apiKeyRepo.setApiKey(apiKey)).fold((failure) {
state = Error(failure, apiKey: state.apiKey);
}, (_) {
state = Loaded(apiKey);
});
}
}

final apiKeyStateNotifierProvider =
StateNotifierProvider<ApiKeyStateNotifier, ApiKeyState>(
(ref) => ApiKeyStateNotifier(ref.watch(apiKeyRepoProvider)),
);
Loading

0 comments on commit 8a1d94b

Please sign in to comment.