From 0fe136013ef28f54ec2f31c8686b97a653255d0a Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Sun, 12 Jan 2025 21:13:01 +0000 Subject: [PATCH] Feat: Remove all ads --- lib/ads/_ads_foss.dart | 95 ---- lib/ads/_iap_foss.dart | 1 - lib/ads/ad_schedule.dart | 30 -- lib/ads/ads.dart | 442 ------------------ lib/ads/age_dialog.dart | 73 --- lib/ads/age_minigame.dart | 203 -------- lib/ads/age_simple_input.dart | 95 ---- lib/ads/iap.dart | 11 - lib/flame/ricochlime_game.dart | 29 -- lib/main.dart | 22 - lib/pages/ad_warning.dart | 51 -- lib/pages/home.dart | 38 +- lib/pages/play.dart | 57 --- lib/pages/settings.dart | 101 ---- lib/pages/shop.dart | 98 ---- lib/utils/prefs.dart | 4 - macos/Flutter/GeneratedPluginRegistrant.swift | 4 - patches/foss.sh | 6 - privacy_policy.md | 24 +- pubspec.lock | 88 ---- pubspec.yaml | 8 - test/bg_skulls_test.dart | 2 - test/game_golden_test.dart | 11 - .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 25 files changed, 7 insertions(+), 1490 deletions(-) delete mode 100644 lib/ads/_ads_foss.dart delete mode 100644 lib/ads/ad_schedule.dart delete mode 100644 lib/ads/ads.dart delete mode 100644 lib/ads/age_dialog.dart delete mode 100644 lib/ads/age_minigame.dart delete mode 100644 lib/ads/age_simple_input.dart delete mode 100644 lib/pages/ad_warning.dart diff --git a/lib/ads/_ads_foss.dart b/lib/ads/_ads_foss.dart deleted file mode 100644 index dfdc6892..00000000 --- a/lib/ads/_ads_foss.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ricochlime/utils/prefs.dart'; - -/// Helper class for ads, -/// with a dummy implementation because ads are disabled. -abstract class AdState { - @visibleForTesting - static bool? forceAdsSupported; - - /// Whether ads are supported on this platform. - static bool get adsSupported => forceAdsSupported ?? false; - - /// Whether we can show rewarded interstitial ads, - /// which is always false. - static const rewardedInterstitialAdsSupported = false; - - /// Whether we should show banner ads. - static Future shouldShowBannerAd() async => false; - - /// The minimum age required to show personalized ads. - static const int minAgeForPersonalizedAds = 13; - - /// The users age, or null if unknown. - static int? get age { - assert(Prefs.birthYear.loaded); - - final birthYear = Prefs.birthYear.value; - if (birthYear == null) return null; - - // Subtract 1 because the user might not have - // had their birthday yet this year. - return DateTime.now().year - birthYear - 1; - } - - /// Initializes ads. - /// - /// This is a no-op. - static void init() {} - - /// Shows the consent form. - /// - /// This is a no-op. - static void showConsentForm() {} - - /// Updates the ad request configuration. - /// - /// This is a no-op. - static Future updateRequestConfiguration() async {} - - /// Shows a rewarded interstitial ad (no-op). - /// - /// Returns whether the reward was earned, - /// which is always false. - static Future showRewardedInterstitialAd() async => false; -} - -/// A widget that displays a banner ad. -/// -/// This is a dummy widget that does nothing. -class BannerAdWidget extends StatelessWidget { - // ignore: public_member_api_docs - const BannerAdWidget({ - super.key, - required this.adSize, - }); - - /// The requested banner ad size. - final AdSize adSize; - - @override - Widget build(BuildContext context) => const SizedBox(); -} - -/// A banner ad size. -/// -/// This is a reimplemented version of -/// `google_mobile_ads`'s `AdSize` class, -/// so there are no compilation errors -/// when the dependency is removed. -class AdSize { - // ignore: public_member_api_docs - const AdSize({ - required this.width, - required this.height, - }); - - /// Standard banner ad size. - static const banner = AdSize(width: 320, height: 50); - - /// The requested banner ad width. - final int width; - - /// The requested banner ad height. - final int height; -} diff --git a/lib/ads/_iap_foss.dart b/lib/ads/_iap_foss.dart index 2fe60d13..570b036e 100644 --- a/lib/ads/_iap_foss.dart +++ b/lib/ads/_iap_foss.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:ricochlime/utils/prefs.dart'; enum RicochlimeProduct { - removeAdsForever('remove_ads_forever', consumable: false), buy1000Coins('buy_1000_coins', consumable: true), buy5000Coins('buy_5000_coins', consumable: true), ; diff --git a/lib/ads/ad_schedule.dart b/lib/ads/ad_schedule.dart deleted file mode 100644 index 67be6322..00000000 --- a/lib/ads/ad_schedule.dart +++ /dev/null @@ -1,30 +0,0 @@ -abstract final class AdSchedule { - static DateTime _lastShown = DateTime.now(); - - /// The minimum time before an ad can be shown. - static const _minTimeBetweenAds = Duration(minutes: 1); - - /// Resets [_lastShown] so ads aren't shown immediately - /// after the app is opened/resumed. - static void onResume(double dt) { - _lastShown = DateTime.now(); - } - - /// Returns whether an interstitial ad should be shown. - static bool enoughTimeSinceLastAd() { - final now = DateTime.now(); - final timeSinceLastAd = now.difference(_lastShown); - - return timeSinceLastAd >= _minTimeBetweenAds; - } - - /// Updates [_lastShown] to the current time. - static void markAdShown() { - _lastShown = DateTime.now(); - } - - /// Updates [_lastShown] to reflect that an ad was cancelled. - static void markAdCancelled() { - _lastShown = DateTime.now().subtract(_minTimeBetweenAds * 0.5); - } -} diff --git a/lib/ads/ads.dart b/lib/ads/ads.dart deleted file mode 100644 index c3917774..00000000 --- a/lib/ads/ads.dart +++ /dev/null @@ -1,442 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:app_tracking_transparency/app_tracking_transparency.dart'; -import 'package:battery_plus/battery_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:google_mobile_ads/google_mobile_ads.dart'; -import 'package:logging/logging.dart'; -import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/ads/iap.dart'; -import 'package:ricochlime/utils/prefs.dart'; -import 'package:ricochlime/utils/ricochlime_palette.dart'; - -export 'package:google_mobile_ads/google_mobile_ads.dart' show AdSize; - -/// Helper class for ads. -abstract class AdState { - static final log = Logger('AdState'); - - static bool _initializeStarted = false; - static bool _initializeCompleted = false; - - static late final String _bannerAdUnitId; - static late final String _rewardedInterstitialAdUnitId; - static RewardedInterstitialAd? _rewardedInterstitialAd; - - @visibleForTesting - static bool? forceAdsSupported; - - /// Whether ads are supported on this platform. - static bool get adsSupported => - forceAdsSupported ?? _bannerAdUnitId.isNotEmpty; - - /// Whether we can show rewarded interstitial ads. - /// This is true if ads are supported and the user is old enough. - static bool get rewardedInterstitialAdsSupported { - if (!adsSupported) return false; - if (RicochlimeProduct.removeAdsForever.state.value == - IAPState.purchasedAndEnabled) { - return false; - } - - final age = AdState.age; - return age != null && age >= minAgeForPersonalizedAds; - } - - /// Whether we should show banner ads. - /// - /// E.g. if the user is in battery save mode, we should not show a - /// banner ad. - static Future shouldShowBannerAd() async { - if (!adsSupported) return false; - if (RicochlimeProduct.removeAdsForever.state.value == - IAPState.purchasedAndEnabled) { - return false; - } - - if (await Battery().isInBatterySaveMode) return false; - - return true; - } - - /// The minimum age required to show personalized ads. - static const int minAgeForPersonalizedAds = 13; - - /// The user's age, - /// calculated from their birth year, - /// or null if the user has not entered their birth year. - static int? get age { - assert(Prefs.birthYear.loaded); - - final birthYear = Prefs.birthYear.value; - if (birthYear == null) return null; - - // Subtract 1 because the user might not have - // had their birthday yet this year. - return DateTime.now().year - birthYear - 1; - } - - /// Initializes ads. - static void init() { - if (kDebugMode) { - // test ads - if (kIsWeb) { - _bannerAdUnitId = ''; - _rewardedInterstitialAdUnitId = ''; - } else if (Platform.isAndroid) { - _bannerAdUnitId = 'ca-app-pub-3940256099942544/6300978111'; - _rewardedInterstitialAdUnitId = - 'ca-app-pub-3940256099942544/5354046379'; - } else if (Platform.isIOS) { - _bannerAdUnitId = 'ca-app-pub-3940256099942544/2934735716'; - _rewardedInterstitialAdUnitId = - 'ca-app-pub-3940256099942544/6978759866'; - } else { - _bannerAdUnitId = ''; - _rewardedInterstitialAdUnitId = ''; - } - assert(_bannerAdUnitId.isEmpty || - _bannerAdUnitId.startsWith('ca-app-pub-3940256099942544')); - assert(_rewardedInterstitialAdUnitId.isEmpty || - _rewardedInterstitialAdUnitId - .startsWith('ca-app-pub-3940256099942544')); - } else { - // actual ads - if (kIsWeb) { - _bannerAdUnitId = ''; - _rewardedInterstitialAdUnitId = ''; - } else if (Platform.isAndroid) { - _bannerAdUnitId = 'ca-app-pub-1312561055261176/8961545046'; - _rewardedInterstitialAdUnitId = - 'ca-app-pub-1312561055261176/1010163793'; - } else if (Platform.isIOS) { - _bannerAdUnitId = 'ca-app-pub-1312561055261176/8306938920'; - _rewardedInterstitialAdUnitId = - 'ca-app-pub-1312561055261176/1654776024'; - } else { - _bannerAdUnitId = ''; - _rewardedInterstitialAdUnitId = ''; - } - assert(_bannerAdUnitId.isEmpty || - _bannerAdUnitId.startsWith('ca-app-pub-1312561055261176')); - assert(_rewardedInterstitialAdUnitId.isEmpty || - _rewardedInterstitialAdUnitId - .startsWith('ca-app-pub-1312561055261176')); - } - - if (adsSupported) _startInitialize(); - } - - static Future _startInitialize() async { - if (_initializeStarted) return; - - final age = AdState.age; - final canConsent = age != null && age >= minAgeForPersonalizedAds; - if (canConsent) { - if (!kIsWeb && Platform.isIOS) { - var status = await AppTrackingTransparency.trackingAuthorizationStatus; - if (status == TrackingStatus.notDetermined) { - // wait to avoid crash - await Future.delayed(const Duration(seconds: 3)); - - status = await AppTrackingTransparency.requestTrackingAuthorization(); - } - if (status == TrackingStatus.authorized) { - _checkForRequiredConsent(); - } - } else { - _checkForRequiredConsent(); - } - } else { - _checkForRequiredConsent(shouldShowConsentForm: false); - } - - Prefs.birthYear.addListener(updateRequestConfiguration); - await updateRequestConfiguration(); - - assert(_bannerAdUnitId.isNotEmpty); - assert(!_initializeCompleted); - _initializeStarted = true; - await MobileAds.instance.initialize(); - _initializeCompleted = true; - - await updateRequestConfiguration(); - - unawaited(_preloadRewardedInterstitialAd()); - } - - static void _checkForRequiredConsent({ - bool shouldShowConsentForm = true, - }) { - final params = ConsentRequestParameters(); - ConsentInformation.instance.requestConsentInfoUpdate( - params, - () async { - final status = await ConsentInformation.instance.getConsentStatus(); - if (status != ConsentStatus.required) return; - if (await ConsentInformation.instance.isConsentFormAvailable()) { - if (shouldShowConsentForm) { - showConsentForm(); - } else { - // otherwise, the form is kept locally and can be shown later - } - } - }, - (formError) {}, - ); - } - - /// Shows the consent form. - /// - /// It is assumed that [_checkForRequiredConsent] - /// has already been called. - static void showConsentForm() { - ConsentForm.loadConsentForm( - (ConsentForm consentForm) async { - consentForm.show((formError) async { - if (formError != null) { - // Handle dismissal by reloading form - showConsentForm(); - } - }); - }, - (formError) {}, - ); - } - - static Future _preloadRewardedInterstitialAd() { - if (!rewardedInterstitialAdsSupported) return Future.value(false); - final completer = Completer(); - RewardedInterstitialAd.load( - adUnitId: _rewardedInterstitialAdUnitId, - request: AdRequest( - nonPersonalizedAds: age == null ? true : null, - ), - rewardedInterstitialAdLoadCallback: RewardedInterstitialAdLoadCallback( - onAdLoaded: (ad) { - log.info('Rewarded interstitial ad loaded!'); - _rewardedInterstitialAd = ad; - completer.complete(true); - }, - onAdFailedToLoad: (error) { - log.warning('Rewarded interstitial ad failed to load: $error', error); - completer.complete(false); - }, - ), - ); - return completer.future; - } - - /// Updates the ad request configuration - /// based on the user's age. - static Future updateRequestConfiguration() async { - final age = AdState.age; - await MobileAds.instance.updateRequestConfiguration(RequestConfiguration( - maxAdContentRating: switch (age) { - null => MaxAdContentRating.pg, // parental guidance - < 13 => MaxAdContentRating.pg, // parental guidance - _ => MaxAdContentRating.t, // teen - }, - tagForChildDirectedTreatment: switch (age) { - null => TagForChildDirectedTreatment.yes, - < 13 => TagForChildDirectedTreatment.yes, - _ => TagForChildDirectedTreatment.no, - }, - tagForUnderAgeOfConsent: switch (age) { - null => TagForUnderAgeOfConsent.yes, - < 13 => TagForUnderAgeOfConsent.yes, - _ => TagForUnderAgeOfConsent.no, - }, - )); - } - - static Future _createBannerAd(AdSize adSize) async { - if (!adsSupported) { - log.warning('Banner ad unit ID is empty.'); - return null; - } else if (RicochlimeProduct.removeAdsForever.state.value == - IAPState.purchasedAndEnabled) { - log.severe('Banner ad should not be created with removeAdsForever.'); - return null; - } else if (!_initializeStarted) { - log.warning('Ad initialization has not started.'); - return null; - } - - while (!_initializeCompleted) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - final bannerAd = BannerAd( - adUnitId: _bannerAdUnitId, - request: AdRequest( - nonPersonalizedAds: age == null ? true : null, - ), - size: adSize, - listener: BannerAdListener( - onAdLoaded: (Ad ad) { - log.fine('Ad loaded!'); - }, - onAdFailedToLoad: (Ad ad, LoadAdError error) { - log.warning('Ad failed to load: $error', error); - ad.dispose(); - }, - ), - ); - - unawaited(bannerAd.load()); - - return bannerAd; - } - - /// Shows a rewarded interstitial ad. - /// - /// Returns whether the reward was earned. - static Future showRewardedInterstitialAd() async { - if (!rewardedInterstitialAdsSupported) return false; - - if (_rewardedInterstitialAd == null) { - log.info('Rewarded interstitial ad is null, loading now...'); - final loaded = await _preloadRewardedInterstitialAd(); - if (!loaded) { - log.warning('Rewarded ad failed to load.'); - return false; - } - assert(_rewardedInterstitialAd != null); - } - - final completer = Completer(); - _rewardedInterstitialAd!.fullScreenContentCallback = - FullScreenContentCallback( - onAdDismissedFullScreenContent: (ad) { - log.info('$ad onAdDismissedFullScreenContent.'); - ad.dispose(); - if (!completer.isCompleted) { - _rewardedInterstitialAd = null; - _preloadRewardedInterstitialAd(); - completer.complete(false); - } - }, - ); - unawaited(_rewardedInterstitialAd!.show( - onUserEarnedReward: (ad, reward) { - ad.dispose(); - _rewardedInterstitialAd = null; - _preloadRewardedInterstitialAd(); - completer.complete(true); - }, - )); - - return completer.future.timeout( - const Duration(minutes: 2), - onTimeout: () { - log.warning('Rewarded interstitial timed out, granting reward anyway.'); - _rewardedInterstitialAd?.dispose(); - _rewardedInterstitialAd = null; - _preloadRewardedInterstitialAd(); - return true; - }, - ); - } -} - -/// A widget that displays a banner ad. -class BannerAdWidget extends StatefulWidget { - // ignore: public_member_api_docs - const BannerAdWidget({ - super.key, - required this.adSize, - }); - - /// The requested banner ad size. - final AdSize adSize; - - @override - State createState() => _BannerAdWidgetState(); -} - -class _BannerAdWidgetState extends State - with AutomaticKeepAliveClientMixin { - BannerAd? _bannerAd; - - @override - void initState() { - super.initState(); - AdState._createBannerAd(widget.adSize).then((bannerAd) { - if (mounted) { - setState(() => _bannerAd = bannerAd); - } else { - _bannerAd = null; - bannerAd?.dispose(); - } - updateKeepAlive(); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - - late final colorScheme = Theme.of(context).colorScheme; - - const nesPadding = EdgeInsets.all(3); - - return FittedBox( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.all(3), - child: Stack( - children: [ - NesContainer( - width: widget.adSize.width + nesPadding.left + nesPadding.right, - height: widget.adSize.height + nesPadding.top + nesPadding.bottom, - padding: nesPadding, - backgroundColor: RicochlimePalette.grassColor, - ), - Positioned.fill( - left: nesPadding.left, - right: nesPadding.right, - top: nesPadding.top, - bottom: nesPadding.bottom, - child: _bannerAd == null - ? Center( - child: FaIcon( - FontAwesomeIcons.rectangleAd, - color: colorScheme.onSurface.withValues(alpha: 0.5), - ), - ) - : ClipRRect( - borderRadius: BorderRadius.circular( - nesPadding.horizontal * 1.5, - ), - child: AdWidget(ad: _bannerAd!), - ), - ), - IgnorePointer( - child: NesContainer( - width: widget.adSize.width + nesPadding.left + nesPadding.right, - height: - widget.adSize.height + nesPadding.top + nesPadding.bottom, - padding: nesPadding, - backgroundColor: Colors.transparent, - ), - ), - ], - ), - ), - ); - } - - @override - void dispose() { - _bannerAd?.dispose(); - _bannerAd = null; - super.dispose(); - } - - @override - bool get wantKeepAlive => _bannerAd != null; -} diff --git a/lib/ads/age_dialog.dart b/lib/ads/age_dialog.dart deleted file mode 100644 index 6c1fea0c..00000000 --- a/lib/ads/age_dialog.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:io'; -import 'dart:math' as math; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/i18n/strings.g.dart'; -import 'package:ricochlime/utils/prefs.dart'; - -part 'age_minigame.dart'; -part 'age_simple_input.dart'; - -/// A dialog which asks the user how old they are. -/// -/// This can either take the form of an -/// age guessing minigame [_AgeMinigame] -/// or a simple text field [_AgeSimpleInput]. -class AgeDialog extends StatefulWidget { - // ignore: public_member_api_docs - const AgeDialog({ - super.key, - this.dismissible = true, - }); - - /// Whether the dialog can be dismissed. - /// - /// This should be `false` if the user hasn't inputted - /// their birth year before. - final bool dismissible; - - @override - State createState() => _AgeDialogState(); -} - -class _AgeDialogState extends State { - /// The minigame is iffy with Google Play policies, - /// so on Android, we use a simple input field instead. - /// - /// The user can still choose to use the minigame. - bool _useMinigame = kIsWeb || !Platform.isAndroid; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Center( - child: NesContainer( - width: 300 + 32 * 2, - padding: const EdgeInsets.all(32), - backgroundColor: colorScheme.surface, - child: Material( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_useMinigame) - const _AgeMinigame() - else - const _AgeSimpleInput(), - const SizedBox(height: 32), - TextButton( - onPressed: () => setState(() => _useMinigame = !_useMinigame), - child: Text( - _useMinigame - ? t.ageDialog.useSimpleInput - : t.ageDialog.useMinigame, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/ads/age_minigame.dart b/lib/ads/age_minigame.dart deleted file mode 100644 index f5971e48..00000000 --- a/lib/ads/age_minigame.dart +++ /dev/null @@ -1,203 +0,0 @@ -part of 'age_dialog.dart'; - -/// This is a small minigame -/// which uses a binary search guessing game. -class _AgeMinigame extends StatefulWidget { - // ignore: public_member_api_docs - const _AgeMinigame({ - // ignore: unused_element - super.key, - }); - - @override - State<_AgeMinigame> createState() => _AgeMinigameState(); -} - -class _AgeMinigameState extends State<_AgeMinigame> { - static const int minLowerBound = 1; - static const int maxUpperBound = 120; - static const int initialAgeGuess = minLowerBound; - int guessNumber = 1; - int lowerBound = minLowerBound; - int upperBound = maxUpperBound; - int guessedAge = initialAgeGuess; - - bool get canGoOlder => lowerBound < upperBound && guessedAge < maxUpperBound; - bool get canGoYounger => - upperBound > lowerBound && guessedAge > minLowerBound; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - t.ageDialog.letMeGuessYourAge, - style: const TextStyle( - fontSize: kToolbarHeight / 2, - ), - ), - const SizedBox(height: 8), - Text(t.ageDialog.reason), - const SizedBox(height: 24), - - // Age guess - Text( - t.ageDialog.guessNumber(n: guessNumber), - style: const TextStyle( - height: 1, - ), - ), - Text( - t.ageDialog.areYou(age: guessedAge), - style: const TextStyle( - fontSize: kToolbarHeight / 2, - ), - ), - const SizedBox(height: 8), - _RangeBar( - currentLowerBound: lowerBound, - currentUpperBound: upperBound, - minLowerBound: minLowerBound, - maxUpperBound: maxUpperBound, - ), - const SizedBox(height: 16), - - NesButton( - type: NesButtonType.warning, - onPressed: canGoOlder - ? () { - setState(() { - guessNumber++; - lowerBound = guessedAge + 1; - guessedAge = (lowerBound + upperBound) ~/ 2; - }); - } - : null, - child: Row( - children: [ - NesIcon( - iconData: NesIcons.topArrowIndicator, - ), - const SizedBox(width: 16), - Text(t.ageDialog.older), - ], - ), - ), - const SizedBox(height: 8), - NesButton( - type: NesButtonType.primary, - onPressed: () { - final currentYear = DateTime.now().year; - final birthYear = currentYear - guessedAge; - Prefs.birthYear.value = birthYear; - Navigator.of(context).pop(); - }, - child: Row( - children: [ - NesIcon( - iconData: NesIcons.check, - ), - const SizedBox(width: 16), - Text(t.ageDialog.yesMyAgeIs(age: guessedAge)) - ], - ), - ), - const SizedBox(height: 8), - NesButton( - type: NesButtonType.warning, - onPressed: canGoYounger - ? () { - setState(() { - guessNumber++; - upperBound = guessedAge - 1; - guessedAge = (lowerBound + upperBound) ~/ 2; - }); - } - : null, - child: Row( - children: [ - NesIcon( - iconData: NesIcons.bottomArrowIndicator, - ), - const SizedBox(width: 16), - Text(t.ageDialog.younger), - ], - ), - ), - const SizedBox(height: 16), - NesButton( - type: NesButtonType.error, - child: Row( - children: [ - NesIcon( - iconData: NesIcons.redo, - ), - const SizedBox(width: 16), - Text(t.ageDialog.reset), - ], - ), - onPressed: () { - setState(() { - guessNumber = 1; - lowerBound = minLowerBound; - upperBound = maxUpperBound; - guessedAge = initialAgeGuess; - }); - }, - ), - ], - ); - } -} - -class _RangeBar extends StatelessWidget { - const _RangeBar({ - // ignore: unused_element - super.key, - required this.currentLowerBound, - required this.currentUpperBound, - required this.minLowerBound, - required this.maxUpperBound, - }); - - final int currentLowerBound; - final int currentUpperBound; - final int minLowerBound; - final int maxUpperBound; - - @override - Widget build(BuildContext context) { - final darkMode = Theme.of(context).brightness == Brightness.dark; - final colorScheme = Theme.of(context).colorScheme; - const barHeight = 2.0; - return LayoutBuilder(builder: (context, constraints) { - return NesContainer( - width: constraints.maxWidth, - height: barHeight, - backgroundColor: colorScheme.primary.withAlpha(100), - padding: EdgeInsets.zero, - child: Stack( - clipBehavior: Clip.none, - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - left: (currentLowerBound / maxUpperBound) * constraints.maxWidth, - width: math.max( - 1, - ((currentUpperBound - currentLowerBound) / maxUpperBound) * - constraints.maxWidth, - ), - top: -1, - bottom: -1, - child: ColoredBox( - color: darkMode ? colorScheme.primary : colorScheme.onPrimary, - ), - ), - ], - ), - ); - }); - } -} diff --git a/lib/ads/age_simple_input.dart b/lib/ads/age_simple_input.dart deleted file mode 100644 index 1be7628a..00000000 --- a/lib/ads/age_simple_input.dart +++ /dev/null @@ -1,95 +0,0 @@ -part of 'age_dialog.dart'; - -/// This is a simple input field -/// to comply with Google Play's policy -/// on having a neutral age screen. -class _AgeSimpleInput extends StatefulWidget { - // ignore: public_member_api_docs - const _AgeSimpleInput({ - // ignore: unused_element - super.key, - }); - - @override - State<_AgeSimpleInput> createState() => _AgeSimpleInputState(); -} - -class _AgeSimpleInputState extends State<_AgeSimpleInput> { - final _inputKey = GlobalKey(); - final _inputController = TextEditingController(); - - String? _validateAge(String? input) { - if (input == null || input.isEmpty) { - return t.ageDialog.invalidAge; - } - final age = int.tryParse(input); - if (age == null) { - return t.ageDialog.invalidAge; - } - if (age < _AgeMinigameState.minLowerBound || - age > _AgeMinigameState.maxUpperBound) { - return t.ageDialog.invalidAge; - } - return null; - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - t.ageDialog.howOldAreYou, - style: const TextStyle( - fontSize: kToolbarHeight / 2, - ), - ), - const SizedBox(height: 8), - Text(t.ageDialog.reason), - const SizedBox(height: 24), - - // Age input - TextFormField( - key: _inputKey, - controller: _inputController, - validator: _validateAge, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: t.ageDialog.yourAge, - ), - ), - const SizedBox(height: 8), - - // Submit button - AnimatedBuilder( - animation: _inputController, - builder: (context, child) { - final age = int.tryParse(_inputController.text); - final valid = _validateAge(_inputController.text) == null; - return NesButton( - type: valid ? NesButtonType.primary : NesButtonType.normal, - onPressed: valid - ? () { - if (_inputKey.currentState?.validate() == false) return; - final currentYear = DateTime.now().year; - final birthYear = currentYear - age!; - Prefs.birthYear.value = birthYear; - Navigator.of(context).pop(); - } - : null, - child: Row( - children: [ - NesIcon( - iconData: NesIcons.check, - ), - const SizedBox(width: 16), - Text(t.ageDialog.yesMyAgeIs(age: age ?? '?')), - ], - ), - ); - }, - ), - ], - ); - } -} diff --git a/lib/ads/iap.dart b/lib/ads/iap.dart index e0fa66c6..561efbb8 100644 --- a/lib/ads/iap.dart +++ b/lib/ads/iap.dart @@ -7,7 +7,6 @@ import 'package:logging/logging.dart'; import 'package:ricochlime/utils/prefs.dart'; enum RicochlimeProduct { - removeAdsForever('remove_ads_forever', consumable: false), buy1000Coins('buy_1000_coins', consumable: true), buy5000Coins('buy_5000_coins', consumable: true), ; @@ -139,16 +138,6 @@ abstract final class RicochlimeIAP { } switch (product) { - case RicochlimeProduct.removeAdsForever: - final state = RicochlimeProduct.removeAdsForever.state; - - if (state.value != IAPState.unpurchased) { - _log.warning( - 'Product already delivered: ${purchaseDetails.productID}'); - } - - state.value = IAPState.purchasedAndEnabled; - case RicochlimeProduct.buy1000Coins: Prefs.addCoins(1000, allowOverMax: true); diff --git a/lib/flame/ricochlime_game.dart b/lib/flame/ricochlime_game.dart index 1dc671f1..f5081c73 100644 --- a/lib/flame/ricochlime_game.dart +++ b/lib/flame/ricochlime_game.dart @@ -12,8 +12,6 @@ import 'package:flutter/material.dart'; // ignore: implementation_imports import 'package:forge2d/src/settings.dart' as physics_settings; import 'package:logging/logging.dart'; -import 'package:ricochlime/ads/ad_schedule.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/flame/components/aim_guide.dart'; import 'package:ricochlime/flame/components/background/background.dart'; import 'package:ricochlime/flame/components/bullet.dart'; @@ -81,7 +79,6 @@ class RicochlimeGame extends Forge2DGame final Ticker ticker = Ticker(); static final timeDilation = ValueNotifier(1); - Future Function()? showAdWarning; Future Function()? showGameOverDialog; /// A completer that completes when all the sprites are loaded. @@ -337,7 +334,6 @@ class RicochlimeGame extends Forge2DGame // ignore: must_call_super (super.update is called in [updateNow]) void update(double dt) { if (dt > maxDt) { - onResume(dt); groupedUpdateDt = 0; fps = 0; // physics engine can't handle such a big dt, so just skip this frame @@ -523,30 +519,6 @@ class RicochlimeGame extends Forge2DGame .any((monster) => monster.position.y >= threshold); } - void onResume(double dt) { - AdSchedule.onResume(dt); - } - - Future showRewardedInterstitial() async { - if (!AdState.rewardedInterstitialAdsSupported) return; - - if (!AdSchedule.enoughTimeSinceLastAd()) return; - - final showAd = await showAdWarning?.call() ?? false; - if (!showAd) { - AdSchedule.markAdCancelled(); - return; - } else { - AdSchedule.markAdShown(); - } - - final rewardGranted = await AdState.showRewardedInterstitialAd(); - if (!rewardGranted) return; - - Prefs.totalAdsWatched.value++; - Prefs.addCoins(100, allowOverMax: true); - } - Future gameOver() async { state.value = GameState.gameOver; assert(!inputAllowed); @@ -615,7 +587,6 @@ class RicochlimeGame extends Forge2DGame /// Saves the high score, /// and clears the current game. void restartGame() { - Future.delayed(const Duration(milliseconds: 100), showRewardedInterstitial); Prefs.highScore.value = max(Prefs.highScore.value, score.value); Prefs.currentGame.value = null; _reset(); diff --git a/lib/main.dart b/lib/main.dart index ce7ebe30..b3add02a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:logging/logging.dart'; -import 'package:ricochlime/ads/ads.dart'; -import 'package:ricochlime/ads/age_dialog.dart'; import 'package:ricochlime/ads/iap.dart'; import 'package:ricochlime/flame/ricochlime_game.dart'; import 'package:ricochlime/i18n/strings.g.dart'; @@ -40,13 +38,11 @@ Future main({ await Future.wait([ LocaleSettings.useDeviceLocale(), Prefs.highScore.waitUntilLoaded(), - Prefs.birthYear.waitUntilLoaded(), GoogleFonts.pendingFonts([GoogleFonts.silkscreenTextTheme()]), RicochlimeGame.instance.preloadSprites.future, ]); unawaited(RicochlimeIAP.init()); - AdState.init(); runApp(TranslationProvider(child: const MyApp())); } @@ -73,24 +69,6 @@ void _addLicenses() { }); } -Future handleCurrentConsentStage(BuildContext context) async { - if (kIsWeb) return; - if (!AdState.adsSupported) return; - - if (Prefs.birthYear.value == null) { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const AgeDialog( - dismissible: false, - ), - ); - assert(Prefs.birthYear.value != null); - } else { - AdState.showConsentForm(); - } -} - class MyApp extends StatefulWidget { const MyApp({super.key}); diff --git a/lib/pages/ad_warning.dart b/lib/pages/ad_warning.dart deleted file mode 100644 index f2b3d6ec..00000000 --- a/lib/pages/ad_warning.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/i18n/strings.g.dart'; -import 'package:ricochlime/nes/coin.dart'; - -class AdWarning extends StatelessWidget { - const AdWarning({ - super.key, - required this.secondsLeft, - required this.cancelAd, - }); - - final ValueNotifier secondsLeft; - final VoidCallback cancelAd; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: ValueListenableBuilder( - valueListenable: secondsLeft, - builder: (context, secondsLeft, _) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - return RichText( - text: TextSpan( - style: textTheme.bodyMedium?.copyWith( - fontSize: 20, - ), - children: [ - t.adWarning.getCoins( - c: const WidgetSpan(child: CoinIcon(size: 24)), - t: TextSpan(text: secondsLeft.toString()), - ), - ], - ), - ); - }, - ), - ), - NesButton( - type: NesButtonType.error, - onPressed: cancelAd, - child: Text(t.common.cancel), - ), - ], - ); - } -} diff --git a/lib/pages/home.dart b/lib/pages/home.dart index c0bd2ce7..254a66d3 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,12 +1,8 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/flame/ricochlime_game.dart'; import 'package:ricochlime/i18n/strings.g.dart'; -import 'package:ricochlime/main.dart'; import 'package:ricochlime/nes/bouncing_icon.dart'; import 'package:ricochlime/pages/play.dart'; import 'package:ricochlime/pages/settings.dart'; @@ -19,21 +15,12 @@ import 'package:ricochlime/utils/shop_items.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); - static bool handledConsent = false; - /// The last known value of the bgm volume, /// excluding when the volume is 0. static double lastKnownOnVolume = 0.7; @override Widget build(BuildContext context) { - if (!handledConsent) { - handledConsent = true; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - handleCurrentConsentStage(context); - }); - } - final colorScheme = Theme.of(context).colorScheme; return AnnotatedRegion( value: SystemUiOverlayStyle( @@ -146,25 +133,6 @@ class _HomePageButton extends StatefulWidget { } class _HomePageButtonState extends State<_HomePageButton> { - /// For the first 2s, buttons are replaced with a loading icon - /// to prevent users trying to click them and getting frustrated - /// when nothing happens. This is because google_mobile_ads seems to block - /// user input while loading for some reason. - late final loadingTimer = - (AdState.adsSupported && !RicochlimeGame.reproducibleGoldenMode) - ? Timer(const Duration(seconds: 2), () { - if (mounted) setState(() {}); - }) - : null; - - bool get loading => loadingTimer?.isActive ?? false; - - @override - void dispose() { - loadingTimer?.cancel(); - super.dispose(); - } - void onPressed() { final route = (!Prefs.stylizedPageTransitions.value || MediaQuery.disableAnimationsOf(context)) @@ -187,13 +155,11 @@ class _HomePageButtonState extends State<_HomePageButton> { Widget build(BuildContext context) { return Center( child: NesButton( - onPressed: loading ? null : onPressed, + onPressed: onPressed, type: widget.type, child: Row( children: [ - if (loading) - const NesHourglassLoadingIndicator() - else if (!RicochlimeGame.reproducibleGoldenMode && + if (!RicochlimeGame.reproducibleGoldenMode && (widget.shouldAnimateIcon?.call() ?? false)) BouncingIcon(icon: NesIcon(iconData: widget.icon)) else diff --git a/lib/pages/play.dart b/lib/pages/play.dart index 87f16b7c..2b57fc27 100644 --- a/lib/pages/play.dart +++ b/lib/pages/play.dart @@ -4,11 +4,9 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/flame/ricochlime_game.dart'; import 'package:ricochlime/i18n/strings.g.dart'; import 'package:ricochlime/nes/coin.dart'; -import 'package:ricochlime/pages/ad_warning.dart'; import 'package:ricochlime/pages/game_over.dart'; import 'package:ricochlime/pages/restart_game.dart'; import 'package:ricochlime/utils/brightness_extension.dart'; @@ -27,7 +25,6 @@ class _PlayPageState extends State { void initState() { super.initState(); RicochlimeGame.instance - ..showAdWarning = showAdWarning ..showGameOverDialog = showGameOverDialog ..resumeBgMusic(); if (RicochlimeGame.instance.state.value == GameState.gameOver) { @@ -48,7 +45,6 @@ class _PlayPageState extends State { @override void dispose() { RicochlimeGame.instance - ..showAdWarning = null ..showGameOverDialog = null ..cancelCurrentTurn() ..pauseBgMusic(); @@ -79,42 +75,6 @@ class _PlayPageState extends State { } } - /// Shows a warning dialog before showing a rewarded interstitial ad. - /// Returns false if the user cancels the ad, true otherwise. - Future showAdWarning() { - final completer = Completer(); - - final secondsLeft = ValueNotifier(3); - final timer = Timer.periodic( - const Duration(seconds: 1), - (timer) { - secondsLeft.value--; - if (secondsLeft.value <= 0) { - timer.cancel(); - if (!completer.isCompleted) completer.complete(true); - Navigator.of(context).pop(); - } - }, - ); - - unawaited(NesBottomSheet.show( - context: context, - maxHeight: .2, - builder: (_) => AdWarning( - secondsLeft: secondsLeft, - cancelAd: () { - timer.cancel(); - if (!completer.isCompleted) completer.complete(false); - Navigator.of(context).pop(); - }, - ), - )); - - return completer.future; - } - - final Future shouldShowBannerAd = AdState.shouldShowBannerAd(); - double _playerPos(Size screenSize) { final fitted = applyBoxFit( BoxFit.contain, @@ -331,23 +291,6 @@ class _PlayPageState extends State { ], ), ), - const SizedBox(height: 2), - FutureBuilder( - future: shouldShowBannerAd, - builder: (context, snapshot) { - final showAd = snapshot.data ?? false; - if (!showAd) return const SizedBox.shrink(); - - final screenSize = MediaQuery.sizeOf(context); - return ConstrainedBox( - constraints: BoxConstraints.tight(Size( - screenSize.width, - (screenSize.height - kToolbarHeight) * 0.1, - )), - child: const BannerAdWidget(adSize: AdSize.banner), - ); - }, - ), ], ), ), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 13db4dfc..a15fb303 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,11 +1,7 @@ import 'dart:math'; -import 'package:collapsible/collapsible.dart'; import 'package:flutter/material.dart'; import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/ads/ads.dart'; -import 'package:ricochlime/ads/age_dialog.dart'; -import 'package:ricochlime/ads/iap.dart'; import 'package:ricochlime/i18n/strings.g.dart'; import 'package:ricochlime/nes/ricochlime_icons.dart'; import 'package:ricochlime/utils/prefs.dart'; @@ -29,11 +25,6 @@ class SettingsPage extends StatelessWidget { top: 16, bottom: 4, ); - const paragraphPadding = EdgeInsets.only( - left: 16, - right: 16, - top: 8, - ); const listTileContentPadding = EdgeInsets.symmetric( horizontal: 16, vertical: 16, @@ -60,98 +51,6 @@ class SettingsPage extends StatelessWidget { ), body: ListView( children: [ - // ad consent - if (AdState.adsSupported) ...[ - Padding( - padding: subtitlePadding, - child: Text( - t.settingsPage.ads, - style: const TextStyle(fontSize: 20), - ), - ), - Padding( - padding: listTilePadding, - child: NesContainer( - padding: EdgeInsets.zero, - child: ListTile( - onTap: () { - showDialog( - context: context, - builder: (context) => const AgeDialog(), - ); - }, - tileColor: listTileColor, - shape: listTileShape, - contentPadding: listTileContentPadding, - title: Text(t.ageDialog.yourAge), - leading: NesIcon( - iconData: NesIcons.user, - ), - trailing: ValueListenableBuilder( - valueListenable: Prefs.birthYear, - builder: (context, birthYear, child) { - return Text( - switch (birthYear) { - null => t.ageDialog.unknown, - _ => '$birthYear', - }, - style: TextStyle( - fontSize: switch (birthYear) { - null => null, - _ => 20, - }, - ), - ); - }, - ), - ), - ), - ), - ValueListenableBuilder( - valueListenable: Prefs.birthYear, - builder: (context, birthYear, child) { - final age = AdState.age; - final collapsed = - age == null || age < AdState.minAgeForPersonalizedAds; - return Collapsible( - collapsed: collapsed, - axis: CollapsibleAxis.vertical, - child: child!, - ); - }, - child: Padding( - padding: listTilePadding, - child: NesContainer( - padding: EdgeInsets.zero, - child: ListTile( - onTap: () { - AdState.showConsentForm(); - }, - tileColor: listTileColor, - shape: listTileShape, - contentPadding: listTileContentPadding, - title: Text(t.settingsPage.adConsent), - leading: NesIcon( - iconData: NesIcons.tv, - ), - ), - ), - ), - ), - if (switch (RicochlimeProduct.removeAdsForever.state.value) { - IAPState.unpurchased => true, - IAPState.purchasedAndDisabled => true, - IAPState.purchasedAndEnabled => false, - }) - Padding( - padding: paragraphPadding, - child: Text( - t.settingsPage.removeAdsInShop, - style: const TextStyle(fontSize: 16), - ), - ), - ], - Padding( padding: subtitlePadding, child: Text( diff --git a/lib/pages/shop.dart b/lib/pages/shop.dart index e310bbca..bd1dd738 100644 --- a/lib/pages/shop.dart +++ b/lib/pages/shop.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:nes_ui/nes_ui.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/ads/iap.dart'; import 'package:ricochlime/i18n/strings.g.dart'; import 'package:ricochlime/nes/coin.dart'; @@ -123,103 +122,6 @@ class ShopPage extends StatelessWidget { ), SliverList.list( children: [ - if (AdState.adsSupported) ...[ - ValueListenableBuilder( - valueListenable: - RicochlimeProduct.removeAdsForever.state, - builder: (context, state, _) { - return NesButton( - type: switch (state) { - IAPState.unpurchased => NesButtonType.warning, - IAPState.purchasedAndEnabled => - NesButtonType.primary, - IAPState.purchasedAndDisabled => - NesButtonType.normal, - }, - onPressed: switch (state) { - IAPState.unpurchased => () { - if (Prefs.coins.value >= - removeAdsForeverCoinPrice) { - Prefs.coins.value -= - removeAdsForeverCoinPrice; - RicochlimeProduct.removeAdsForever.state - .value = IAPState.purchasedAndEnabled; - } else if (RicochlimeIAP - .inAppPurchasesSupported) { - RicochlimeIAP.buy( - RicochlimeProduct.removeAdsForever); - } - }, - IAPState.purchasedAndEnabled => () => - RicochlimeProduct.removeAdsForever.state - .value = IAPState.purchasedAndDisabled, - IAPState.purchasedAndDisabled => () => - RicochlimeProduct.removeAdsForever.state - .value = IAPState.purchasedAndEnabled, - }, - child: Row( - children: [ - NesIcon(iconData: NesIcons.eraser), - const SizedBox(width: 8), - Expanded( - child: Text( - t.shopPage.removeAdsForever, - style: const TextStyle(fontSize: 20), - ), - ), - const SizedBox(width: 8), - switch (state) { - IAPState.unpurchased => - ValueListenableBuilder( - valueListenable: Prefs.coins, - builder: (context, coins, _) { - if (coins >= - removeAdsForeverCoinPrice || - !RicochlimeIAP - .inAppPurchasesSupported) { - return Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - const CoinIcon(size: 20), - Text( - // ignore: prefer_interpolation_to_compose_strings - (removeAdsForeverCoinPrice / - 1000) - .toStringAsFixed(1) + - 'k', - style: const TextStyle( - fontSize: 20), - ), - ], - ); - } else { - return Text( - RicochlimeProduct - .removeAdsForever.price, - style: - const TextStyle(fontSize: 20), - ); - } - }, - ), - IAPState.purchasedAndEnabled => NesCheckBox( - value: true, - onChange: (_) {}, - ), - IAPState.purchasedAndDisabled => - NesCheckBox( - value: false, - onChange: (_) {}, - ), - }, - ], - ), - ); - }, - ), - const SizedBox(height: 8), - ], NesButton( type: NesButtonType.normal, onPressed: () => diff --git a/lib/utils/prefs.dart b/lib/utils/prefs.dart index 8c5fc8dd..4936d0e6 100644 --- a/lib/utils/prefs.dart +++ b/lib/utils/prefs.dart @@ -32,8 +32,6 @@ abstract class Prefs { static late final PlainPref stylizedPageTransitions; static late final PlainPref biggerBullets; - static late final PlainPref birthYear; - static late final PlainPref bgmVolume; static late final PlainPref showUndoButton, showReflectionInAimGuide; @@ -78,8 +76,6 @@ abstract class Prefs { stylizedPageTransitions = PlainPref('stylizedPageTransitions', true); biggerBullets = PlainPref('biggerBullets', false); - birthYear = PlainPref('birthYear', null); - bgmVolume = PlainPref('bgmVolume', 0); showUndoButton = PlainPref('showUndoButton', true); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c4b4ad16..c6550037 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,25 +6,21 @@ import FlutterMacOS import Foundation import audioplayers_darwin -import battery_plus import in_app_purchase_storekit import macos_window_utils import path_provider_foundation import screen_retriever_macos import shared_preferences_foundation import url_launcher_macos -import webview_flutter_wkwebview import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) - BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/patches/foss.sh b/patches/foss.sh index 35c2d190..fb6a2a51 100755 --- a/patches/foss.sh +++ b/patches/foss.sh @@ -1,12 +1,6 @@ -echo "Removing google_mobile_ads package" -sed -i -e '/google_mobile_ads/d' pubspec.yaml - echo "Removing in_app_purchase package" sed -i -e '/in_app_purchase/d' pubspec.yaml -echo "Replacing ads with dummy widgets" -mv lib/ads/_ads_foss.dart lib/ads/ads.dart - echo "Replacing iaps with dummy class" mv lib/ads/_iap_foss.dart lib/ads/iap.dart diff --git a/privacy_policy.md b/privacy_policy.md index febb84e7..59b8a728 100644 --- a/privacy_policy.md +++ b/privacy_policy.md @@ -10,28 +10,14 @@ Ricochlime does not collect any data about you (besides the data collected by th ### Google Ads -Ricochlime is free to download and play, and uses Google AdMob to display ads. AdMob may collect data about you, including your device's advertising ID, IP address, and location. You can read more about [how Google uses data from apps that use their services](https://policies.google.com/technologies/partner-sites). +Ricochlime is free to download and play, and used Google AdMob to display ads in versions v1.11.5 and older on some platforms. +Newer versions of the app do not contain ads. -When you first open Ricochlime, you'll be shown a consent prompt asking you to choose between personalized and non-personalized ads. You can change this at any time by going to the settings menu in the game. - -Note that ads are only used on the Android and iOS versions of Ricochlime: -other platforms (including the web) don't use any ads. -The FOSS (free and open-source software) version of Ricochlime for Android -distributed on F-Droid doesn't use ads either. - -#### Ads policy +AdMob may collect data about you, including your device's advertising ID, IP address, and location. You can read more about [how Google uses data from apps that use their services](https://policies.google.com/technologies/partner-sites). -While Ricochlime is supported by ads, I've tried to make them as unobtrusive as possible. - -At the bottom of the gameplay screen, there is a small -[banner ad](https://support.google.com/admob/answer/9993556). -To save battery and improve performance, -this ad isn't shown at all if your device is in a low-power mode. +When you first open Ricochlime, you'll be shown a consent prompt asking you to choose between personalized and non-personalized ads. You can change this at any time by going to the settings menu in the game. -After every game-over, you will be shown a fullscreen ad -(a "[rewarded interstitial ad](https://support.google.com/admob/answer/9884467)"). -You will be shown a prompt before the ad is shown, and you can choose to skip it -for no penalty or watch it and be rewarded with some in-game coins. +Users are recommended to update their app to the latest version for an ad-free experience. ## Source code diff --git a/pubspec.lock b/pubspec.lock index db31ba0e..e38d4afd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - app_tracking_transparency: - dependency: "direct main" - description: - name: app_tracking_transparency - sha256: "1f71f4d8402552fbf8b191d4edab301f233c1af794878b7bc56c708470ffd74c" - url: "https://pub.dev" - source: hosted - version: "2.0.6+1" archive: dependency: transitive description: @@ -89,22 +81,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - battery_plus: - dependency: "direct main" - description: - name: battery_plus - sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551 - url: "https://pub.dev" - source: hosted - version: "6.2.1" - battery_plus_platform_interface: - dependency: transitive - description: - name: battery_plus_platform_interface - sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 - url: "https://pub.dev" - source: hosted - version: "2.0.1" boolean_selector: dependency: transitive description: @@ -137,14 +113,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - collapsible: - dependency: "direct main" - description: - name: collapsible - sha256: d57126aba739f918562dd9e2ce8435cbfc446d52cf02befcaf22f75c01824ee8 - url: "https://pub.dev" - source: hosted - version: "1.0.0" collection: dependency: transitive description: @@ -177,14 +145,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" equatable: dependency: transitive description: @@ -333,14 +293,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" - google_mobile_ads: - dependency: "direct main" - description: - name: google_mobile_ads - sha256: "4775006383a27a5d86d46f8fb452bfcb17794fc0a46c732979e49a8eb1c8963f" - url: "https://pub.dev" - source: hosted - version: "5.2.0" http: dependency: transitive description: @@ -835,14 +787,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" - upower: - dependency: transitive - description: - name: upower - sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf - url: "https://pub.dev" - source: hosted - version: "0.7.0" url_launcher: dependency: transitive description: @@ -947,38 +891,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - webview_flutter: - dependency: transitive - description: - name: webview_flutter - sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 - url: "https://pub.dev" - source: hosted - version: "4.9.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" - url: "https://pub.dev" - source: hosted - version: "3.16.9" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d - url: "https://pub.dev" - source: hosted - version: "2.10.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb - url: "https://pub.dev" - source: hosted - version: "3.16.3" window_manager: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3ce3e4d2..f476875b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,12 +28,6 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - app_tracking_transparency: ^2.0.4 - - battery_plus: ^6.0.1 - - collapsible: ^1.0.0 - cupertino_icons: ^1.0.2 flame: ^1.8.1 @@ -51,8 +45,6 @@ dependencies: google_fonts: ^6.1.0 - google_mobile_ads: ^5.0.0 - in_app_purchase: ^3.2.0 logging: ^1.2.0 diff --git a/test/bg_skulls_test.dart b/test/bg_skulls_test.dart index 49b948ac..8349dc8a 100644 --- a/test/bg_skulls_test.dart +++ b/test/bg_skulls_test.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/flame/ricochlime_game.dart'; import 'package:ricochlime/utils/prefs.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -13,7 +12,6 @@ void main() { SharedPreferences.setMockInitialValues({}); Prefs.testingMode = true; Prefs.init(); - AdState.init(); RicochlimeGame.disableBgMusic = true; RicochlimeGame.reproducibleGoldenMode = true; RicochlimeGame.instance.random = Random(123); diff --git a/test/game_golden_test.dart b/test/game_golden_test.dart index 3d7c5aaa..e38547cc 100644 --- a/test/game_golden_test.dart +++ b/test/game_golden_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_screenshot/golden_screenshot.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:ricochlime/ads/ads.dart'; import 'package:ricochlime/ads/iap.dart'; import 'package:ricochlime/flame/game_data.dart'; import 'package:ricochlime/flame/ricochlime_game.dart'; @@ -75,7 +74,6 @@ void main() { SharedPreferences.setMockInitialValues({}); Prefs.testingMode = true; Prefs.init(); - AdState.init(); RicochlimeGame.disableBgMusic = true; RicochlimeGame.reproducibleGoldenMode = true; @@ -87,7 +85,6 @@ void main() { ]); }); - Prefs.birthYear.value = 2000; Prefs.coins.value = 493; Prefs.highScore.value = 62; @@ -143,7 +140,6 @@ void _testGame({ final device = goldenDevice.device; RicochlimeIAP.forceInAppPurchasesSupported = goldenDevice.enableIAPs; RicochlimeProduct.init(); - AdState.forceAdsSupported = goldenDevice.enableAds; if (gameSave != null) { Prefs.currentGame.value = GameData.fromJson(jsonDecode(gameSave)); @@ -199,11 +195,4 @@ extension _GoldenScreenshotDevices on GoldenScreenshotDevices { GoldenScreenshotDevices.newerIpad => true, _ => false, }; - bool get enableAds => switch (this) { - GoldenScreenshotDevices.olderIphone => true, - GoldenScreenshotDevices.newerIphone => true, - GoldenScreenshotDevices.olderIpad => true, - GoldenScreenshotDevices.newerIpad => true, - _ => false, - }; } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e8f2b4a4..54a71e8b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -15,8 +14,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); - BatteryPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1a514cf4..b2d5edd1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows - battery_plus screen_retriever_windows url_launcher_windows window_manager