Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/src/extensions/num_extension.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import 'package:intl/intl.dart';

extension NumExt on num {
String get formatted => (this == toInt() ? toInt() : this).toString();

String toCurrency(String currency) => NumberFormat.simpleCurrency(name: currency).format(this);

String toPercentage() => '$this%';
}
26 changes: 24 additions & 2 deletions lib/src/mixins/ads_mixin.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import 'package:azure_devops/src/models/amazon/amazon_item.dart';
import 'package:azure_devops/src/services/ads_service.dart';
import 'package:azure_devops/src/widgets/ad_widget.dart';
import 'package:flutter/widgets.dart';

mixin AdsMixin {
List<AdWithKey> nativeAds = [];
List<AmazonItem> amazonAds = [];
var _hasAmazonAds = false;

/// Load new native ads and map them to [AdWithKey] objects with a new global key to force refresh the UI.
Future<void> getNewNativeAds(AdsService ads) async {
_hasAmazonAds = ads.hasAmazonAds;

if (_hasAmazonAds) {
final items = await _getNewAmazonAds(ads);
if (items.isEmpty) {
_hasAmazonAds = false;
await _getNewAdmobAds(ads);
}
} else {
await _getNewAdmobAds(ads);
}
}

/// Load new native ads and map them to [AdWithKey] objects with a new global key to force refresh the UI.
Future<void> _getNewAdmobAds(AdsService ads) async {
final newAds = await ads.getNewNativeAds();
nativeAds = newAds.map((ad) => (ad: ad, key: GlobalKey())).toList();
}

Future<List<AmazonItem>> _getNewAmazonAds(AdsService ads) async {
final newAmazonAds = await ads.getNewAmazonAds();
return amazonAds = newAmazonAds.toList();
}

/// Whether to show a native ad at the given [index] inside [items] list.
bool shouldShowNativeAd<T>(List<T> items, T item, int index) =>
items.indexOf(item) % 5 == 4 && item != items.first && index < nativeAds.length;
items.indexOf(item) % 5 == 4 && item != items.first && index < (_hasAmazonAds ? amazonAds : nativeAds).length;

Future<void> showInterstitialAd(AdsService ads, {VoidCallback? onDismiss}) async {
await ads.showInterstitialAd(onDismiss: onDismiss);
Expand Down
78 changes: 78 additions & 0 deletions lib/src/models/amazon/amazon_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'dart:convert';

class AmazonItem {
AmazonItem({
required this.id,
required this.itemUrl,
required this.title,
required this.imageUrl,
required this.isPrime,
required this.originalPrice,
required this.discount,
required this.discountedPrice,
required this.currency,
});

factory AmazonItem.fromJson(Map<String, dynamic> json) => AmazonItem(
id: json['id'] as String? ?? '',
itemUrl: json['itemUrl'] as String? ?? '',
title: json['title'] as String? ?? '',
imageUrl: json['imageUrl'] as String? ?? '',
isPrime: json['isPrime'] as bool? ?? false,
originalPrice: Price.fromJson(json['originalPrice'] as Map<String, dynamic>? ?? {}),
discount: json['discount'] == null ? null : Discount.fromJson(json['discount'] as Map<String, dynamic>? ?? {}),
discountedPrice: Price.fromJson(json['discountedPrice'] as Map<String, dynamic>? ?? {}),
currency: json['currency'] as String? ?? '',
);

static List<AmazonItem> listFromJson(String str) => List<AmazonItem>.from(
(json.decode(str) as List<dynamic>? ?? []).map((x) => AmazonItem.fromJson(x as Map<String, dynamic>? ?? {})),
);

final String id;
final String itemUrl;
final String title;
final String imageUrl;
final bool isPrime;
final Price originalPrice;
final Discount? discount;
final Price discountedPrice;
final String currency;

@override
bool operator ==(covariant AmazonItem other) {
if (identical(this, other)) return true;

return other.id == id;
}

@override
int get hashCode {
return id.hashCode;
}
}

class Discount {
Discount({
required this.amount,
required this.percentage,
});

factory Discount.fromJson(Map<String, dynamic> json) => Discount(
amount: (json['amount'] as num?)?.toDouble() ?? 0,
percentage: json['percentage'] as int? ?? 0,
);

final double amount;
final int percentage;
}

class Price {
Price({required this.amount});

factory Price.fromJson(Map<String, dynamic> json) => Price(
amount: (json['amount'] as num?)?.toDouble() ?? 0,
);

final double amount;
}
4 changes: 2 additions & 2 deletions lib/src/screens/commits/screen_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ class _CommitsScreen extends StatelessWidget {
);

if (ctrl.shouldShowNativeAd(commits, c, adsIndex)) {
yield NativeAdWidget(
ad: ctrl.nativeAds[adsIndex++],
yield CustomAdWidget(
item: ctrl.ads.hasAmazonAds ? ctrl.amazonAds[adsIndex++] : ctrl.nativeAds[adsIndex++],
);
}
},
Expand Down
4 changes: 2 additions & 2 deletions lib/src/screens/pipelines/screen_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ class _PipelinesScreen extends StatelessWidget {
);

if (ctrl.shouldShowNativeAd(pipelines, p, adsIndex)) {
yield NativeAdWidget(
ad: ctrl.nativeAds[adsIndex++],
yield CustomAdWidget(
item: ctrl.ads.hasAmazonAds ? ctrl.amazonAds[adsIndex++] : ctrl.nativeAds[adsIndex++],
);
}
},
Expand Down
4 changes: 2 additions & 2 deletions lib/src/screens/profile/screen_profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ class _ProfileScreen extends StatelessWidget {
);

if (ctrl.shouldShowNativeAd(commits, c, adsIndex)) {
yield NativeAdWidget(
ad: ctrl.nativeAds[adsIndex++],
yield CustomAdWidget(
item: ctrl.ads.hasAmazonAds ? ctrl.amazonAds[adsIndex++] : ctrl.nativeAds[adsIndex++],
);
}
},
Expand Down
4 changes: 2 additions & 2 deletions lib/src/screens/pull_requests/screen_pull_requests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ class _PullRequestsScreen extends StatelessWidget {
);

if (ctrl.shouldShowNativeAd(prs, pr, adsIndex)) {
yield NativeAdWidget(
ad: ctrl.nativeAds[adsIndex++],
yield CustomAdWidget(
item: ctrl.ads.hasAmazonAds ? ctrl.amazonAds[adsIndex++] : ctrl.nativeAds[adsIndex++],
);
}
},
Expand Down
4 changes: 2 additions & 2 deletions lib/src/screens/work_items/screen_work_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ class _WorkItemsScreen extends StatelessWidget {
);

if (ctrl.shouldShowNativeAd(items, i, adsIndex)) {
yield NativeAdWidget(
ad: ctrl.nativeAds[adsIndex++],
yield CustomAdWidget(
item: ctrl.ads.hasAmazonAds ? ctrl.amazonAds[adsIndex++] : ctrl.nativeAds[adsIndex++],
);
}
}).toList(),
Expand Down
24 changes: 24 additions & 0 deletions lib/src/services/ads_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import 'dart:io';

import 'package:azure_devops/src/extensions/context_extension.dart';
import 'package:azure_devops/src/mixins/logger_mixin.dart';
import 'package:azure_devops/src/models/amazon/amazon_item.dart';
import 'package:azure_devops/src/router/router.dart';
import 'package:azure_devops/src/services/amazon_service.dart';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

Expand All @@ -15,11 +17,13 @@ const _androidNativeAdId = String.fromEnvironment('ADMOB_NATIVE_ADID_ANDROID');
const _iosNativeAdId = String.fromEnvironment('ADMOB_NATIVE_ADID_IOS');

abstract interface class AdsService {
bool get hasAmazonAds;
Future<void> init();
Future<void> showInterstitialAd({VoidCallback? onDismiss});
void removeAds();
void reactivateAds();
Future<List<AdWithView>> getNewNativeAds();
Future<List<AmazonItem>> getNewAmazonAds();
}

class AdsServiceImpl with AppLogger implements AdsService {
Expand All @@ -38,6 +42,10 @@ class AdsServiceImpl with AppLogger implements AdsService {

bool _showAds = true;

@override
bool get hasAmazonAds => _hasAmazonAds;
bool _hasAmazonAds = true;

@override
Future<void> init() async {
setTag(_tag);
Expand Down Expand Up @@ -170,6 +178,22 @@ class AdsServiceImpl with AppLogger implements AdsService {
return compl.future;
}

@override
Future<List<AmazonItem>> getNewAmazonAds() async {
if (!_showAds) return [];
if (!_hasAmazonAds) return [];

final items = await AmazonService().getItems();
if (items.isEmpty) {
logDebug('No Amazon ads found');
_hasAmazonAds = false;
} else {
_hasAmazonAds = true;
}

return items;
}

void _handleAdFailedToLoad(LoadAdError error, String adType) {
final errorCode = error.code;
final errorMessage = error.message;
Expand Down
56 changes: 56 additions & 0 deletions lib/src/services/amazon_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:azure_devops/src/extensions/reponse_extension.dart';
import 'package:azure_devops/src/mixins/logger_mixin.dart';
import 'package:azure_devops/src/models/amazon/amazon_item.dart';
import 'package:http/http.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

class AmazonService with AppLogger {
factory AmazonService() {
return instance ??= AmazonService._();
}

AmazonService._() {
setTag('AmazonService');
}

static AmazonService? instance;

final _client = SentryHttpClient();

List<AmazonItem>? _items;

static const _url = 'https://products.azdevops.app/api/products?category=all';

var _lastFetchTime = DateTime.now();

void dispose() {
instance = null;
}

Future<List<AmazonItem>> getItems() async {
if (_items != null && _hasFetchedRecently()) return _items!;

final Response jsonsRes;

try {
jsonsRes = await _client.get(Uri.parse(_url));
} catch (e, s) {
logError(e, s);
return [];
}

if (jsonsRes.isError) {
logErrorMessage('Error fetching items');
return [];
}

_lastFetchTime = DateTime.now();

final items = AmazonItem.listFromJson(jsonsRes.body);
logDebug('Fetched ${items.length} items.');

return _items = items;
}

bool _hasFetchedRecently() => _lastFetchTime.isAfter(DateTime.now().subtract(Duration(hours: 1)));
}
Loading
Loading