diff --git a/docs/store/iap_review_remove_ads.png b/docs/store/iap_review_remove_ads.png new file mode 100644 index 0000000..8557042 Binary files /dev/null and b/docs/store/iap_review_remove_ads.png differ diff --git a/test/tool/generate_iap_review_screenshot_test.dart b/test/tool/generate_iap_review_screenshot_test.dart new file mode 100644 index 0000000..7f27881 --- /dev/null +++ b/test/tool/generate_iap_review_screenshot_test.dart @@ -0,0 +1,128 @@ +// Headless generator for the App Store IAP review screenshot: renders the +// Settings screen (where "광고 제거 / Remove Ads" is purchased) to a PNG. +// +// flutter test test/tool/generate_iap_review_screenshot_test.dart +// +// Output: docs/store/iap_review_remove_ads.png +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:block_seasons/data/save_repository.dart'; +import 'package:block_seasons/l10n/gen/app_localizations.dart'; +import 'package:block_seasons/services/iap_service.dart'; +import 'package:block_seasons/state/providers.dart'; +import 'package:block_seasons/ui/screens/settings_screen.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// A stand-in IAP service that reports the product as available with a price, +// so the Settings row renders exactly as a shopper sees it. initialize() is +// never called, so no real store query happens. +class _FakeIap extends IapService { + _FakeIap() : super(onEntitlementGranted: _noop); + static Future _noop() async {} + + @override + bool get available => true; + + @override + ProductDetails get product => ProductDetails( + id: 'remove_ads', + title: 'Remove Ads', + description: 'Remove banner and interstitial ads', + price: r'$0.99', + rawPrice: 0.99, + currencyCode: 'USD', + ); +} + +Future _loadFont() async { + final bytes = + File('/System/Library/Fonts/Supplemental/Arial.ttf').readAsBytesSync(); + final loader = FontLoader('Arial') + ..addFont(Future.value(ByteData.view(bytes.buffer))); + await loader.load(); +} + +Widget _wrap(ProviderContainer c, Widget screen, GlobalKey key) => + UncontrolledProviderScope( + container: c, + child: RepaintBoundary( + key: key, + child: MaterialApp( + debugShowCheckedModeBanner: false, + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: ThemeData( + fontFamily: 'Arial', + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF5B7FFF), + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + home: screen, + ), + ), + ); + +void main() { + testWidgets('generate IAP review screenshot', (tester) async { + // in_app_purchase only registers a native platform for android/iOS/macOS; + // on a desktop target it registers none, so constructing the real + // IapService never makes a store channel call (which would throw async in + // the headless binding). The fake overrides available/product anyway. + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + + await _loadFont(); + + const dpr = 3.0; + tester.view.devicePixelRatio = dpr; + tester.view.physicalSize = const Size(1242, 2688); // 6.5" — accepted size + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + SharedPreferences.setMockInitialValues({}); + final repo = SaveRepository(await SharedPreferences.getInstance()); + + final key = GlobalKey(); + final c = ProviderContainer(overrides: [ + saveRepositoryProvider.overrideWithValue(repo), + iapServiceProvider.overrideWithValue(_FakeIap()), + ]); + await tester.pumpWidget(_wrap(c, const SettingsScreen(), key)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 450)); + + var ro = key.currentContext!.findRenderObject()!; + while (!ro.isRepaintBoundary) { + ro = ro.parent!; + } + final layer = ro.debugLayer! as OffsetLayer; + final bounds = ro.paintBounds; + final bytes = await tester.runAsync(() async { + final image = await layer.toImage(bounds, pixelRatio: dpr); + final data = await image.toByteData(format: ui.ImageByteFormat.png); + image.dispose(); + return data; + }); + File('docs/store/iap_review_remove_ads.png') + ..parent.createSync(recursive: true) + ..writeAsBytesSync(bytes!.buffer.asUint8List()); + c.dispose(); + + // Reset before the body ends so flutter_test's foundation-var invariant + // check (which runs before addTearDown) passes. + debugDefaultTargetPlatformOverride = null; + + expect( + File('docs/store/iap_review_remove_ads.png').existsSync(), isTrue); + }); +}