fix(paywall): add dismiss controls and honest premium copy
The premium sheet had no close affordance or way to defer, and its copy only said "Función Premium" without stating what it unlocks. Adds a header close (X) button and a "not now" secondary action so dismissal is never harder than purchasing, and replaces the bare title with a concrete, honest breakdown of the 5 things premium unlocks (no ads, Android Auto, station recording, alarm vacation ranges, unlimited alarms) plus the one-time-purchase framing. The phone equalizer is never listed, since it stays free for everyone. New l10n keys added to all 13 locales.
This commit is contained in:
@@ -9,6 +9,12 @@ import 'package:pluriwave/widgets/hoja_premium.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// fix/import-alarmas-y-paywall: a purchase sheet the user cannot escape is
|
||||
/// a dark pattern and a Play policy risk. These cover the dismiss
|
||||
/// affordance, the honest/concrete feature list, and the equalizer guard
|
||||
/// (the phone equalizer is free for everyone and must never be presented as
|
||||
/// a premium feature — this has regressed conceptually before).
|
||||
|
||||
/// Fake [PuertoCompras] (mirrors `app_test.dart`'s own fake): lets a test
|
||||
/// drive [EstadoEntitlement]'s purchase-stream events without touching
|
||||
/// `in_app_purchase`.
|
||||
@@ -68,6 +74,150 @@ void main() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
/// Presents `HojaPremium` through the REAL `mostrarHojaPremium` modal
|
||||
/// route (unlike [bombear], which embeds it directly with no route to
|
||||
/// pop) so the dismiss controls can be exercised end-to-end exactly as a
|
||||
/// user would encounter them.
|
||||
Future<void> bombearComoHoja(
|
||||
WidgetTester tester, {
|
||||
required _PuertoComprasFalso compras,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoEntitlement>(
|
||||
create: (_) => EstadoEntitlement(prefs: null, compras: compras),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder:
|
||||
(context) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () => mostrarHojaPremium(context),
|
||||
child: const Text('abrir'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('abrir'));
|
||||
await tester.pumpAndSettle();
|
||||
l10n = AppLocalizations.of(tester.element(find.byType(HojaPremium)));
|
||||
}
|
||||
|
||||
group('fix/import-alarmas-y-paywall — dismissibility', () {
|
||||
testWidgets('the close (X) control closes the sheet without purchasing or '
|
||||
'restoring', (tester) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombearComoHoja(tester, compras: compras);
|
||||
|
||||
expect(find.byType(HojaPremium), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('hoja-premium-cerrar')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HojaPremium), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the "not now" secondary action closes the sheet without purchasing '
|
||||
'or restoring',
|
||||
(tester) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombearComoHoja(tester, compras: compras);
|
||||
|
||||
expect(find.byType(HojaPremium), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('hoja-premium-ahora-no')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HojaPremium), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('the system back gesture also closes the sheet (default '
|
||||
'isDismissible/enableDrag, no PopScope blocking it)', (tester) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombearComoHoja(tester, compras: compras);
|
||||
|
||||
expect(find.byType(HojaPremium), findsOneWidget);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HojaPremium), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'an already-premium user still gets the close control (no decline '
|
||||
'needed) and no "not now" button',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({'compra_premium_v1': true});
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombearComoHoja(tester, compras: compras);
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('hoja-premium-cerrar')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('hoja-premium-ahora-no')),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('fix/import-alarmas-y-paywall — honest, concrete copy', () {
|
||||
testWidgets('lists the 5 features premium actually unlocks', (
|
||||
tester,
|
||||
) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombear(tester, compras: compras);
|
||||
|
||||
expect(find.text(l10n.premiumBeneficioSinAnuncios), findsOneWidget);
|
||||
expect(find.text(l10n.premiumBeneficioAndroidAuto), findsOneWidget);
|
||||
expect(find.text(l10n.premiumBeneficioGrabacion), findsOneWidget);
|
||||
expect(find.text(l10n.premiumBeneficioVacaciones), findsOneWidget);
|
||||
expect(find.text(l10n.premiumBeneficioAlarmasIlimitadas), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('states this is a one-time purchase, not a subscription', (
|
||||
tester,
|
||||
) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombear(tester, compras: compras);
|
||||
|
||||
expect(find.text(l10n.premiumPagoUnico), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'GUARD: the phone equalizer is never presented as a premium feature',
|
||||
(tester) async {
|
||||
final compras = _PuertoComprasFalso();
|
||||
addTearDown(compras.dispose);
|
||||
await bombear(tester, compras: compras);
|
||||
|
||||
expect(find.text(l10n.equalizerTitle), findsNothing);
|
||||
expect(find.text(l10n.equalizerActive), findsNothing);
|
||||
expect(find.textContaining('cualizador'), findsNothing);
|
||||
expect(find.textContaining('qualizer'), findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group(
|
||||
'FIX 9 — la etiqueta de premium activo es propia, no la del ecualizador',
|
||||
() {
|
||||
|
||||
Reference in New Issue
Block a user