Add Skip Forever, app icon, About screen links, release signing
- Add Skip/Skip Forever links to onboarding screens only - Remove Skip links from questionnaire screens (baseline, behavioral, lifestyle) - Add app icon (1024x1024) and generate all platform sizes - Update About screen with correct URLs (privacy, terms, disclaimer, how it works) - Add "No ads. Ever." section to About screen - Configure Android release signing with upload keystore - Add URL intent query for Android 11+ link launching - Add skipForever preference to LocalStorage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2
.gitignore
vendored
|
|
@ -43,3 +43,5 @@ app.*.map.json
|
|||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
upload-keystore.jks
|
||||
android/key.properties
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
|
|
@ -5,6 +8,12 @@ plugins {
|
|||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.payfrit.add_months"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
|
|
@ -30,11 +39,18 @@ android {
|
|||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,9 @@
|
|||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 989 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
<color name="ic_launcher_background">#2D2D2D</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 4 MiB After Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 456 B After Width: | Height: | Size: 630 B |
|
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 977 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -5,8 +5,10 @@ import '../theme.dart';
|
|||
class AboutScreen extends StatelessWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
static const String _helpUrl = 'https://addmonths.app/help';
|
||||
static const String _privacyUrl = 'https://addmonths.app/privacy';
|
||||
static const String _howItWorksUrl = 'https://addmonths.com/#how-it-works';
|
||||
static const String _privacyUrl = 'https://addmonths.com/privacy/';
|
||||
static const String _termsUrl = 'https://addmonths.com/terms/';
|
||||
static const String _disclaimerUrl = 'https://addmonths.com/disclaimer/';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -112,14 +114,50 @@ class AboutScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// No ads promise
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.block,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'No ads. Ever.',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'No trackers, no monetization schemes. '
|
||||
'Just a simple tool that respects your time.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Links
|
||||
_buildLinkButton(
|
||||
context,
|
||||
icon: Icons.help_outline,
|
||||
label: 'How it works',
|
||||
onTap: () => _launchUrl(_helpUrl),
|
||||
label: 'How It Works',
|
||||
onTap: () => _launchUrl(_howItWorksUrl),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildLinkButton(
|
||||
|
|
@ -128,23 +166,19 @@ class AboutScreen extends StatelessWidget {
|
|||
label: 'Privacy Policy',
|
||||
onTap: () => _launchUrl(_privacyUrl),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Disclaimer
|
||||
Text(
|
||||
'Disclaimer',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'This app provides general information based on population-level '
|
||||
'research and is not medical advice. Individual results vary widely. '
|
||||
'Consult a healthcare provider for personalized guidance.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
const SizedBox(height: 12),
|
||||
_buildLinkButton(
|
||||
context,
|
||||
icon: Icons.description_outlined,
|
||||
label: 'Terms of Service',
|
||||
onTap: () => _launchUrl(_termsUrl),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildLinkButton(
|
||||
context,
|
||||
icon: Icons.medical_information_outlined,
|
||||
label: 'Medical Disclaimer',
|
||||
onTap: () => _launchUrl(_disclaimerUrl),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -484,4 +484,5 @@ class _BaselineScreenState extends State<BaselineScreen> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -336,4 +336,5 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,4 +361,5 @@ class _LifestyleScreenState extends State<LifestyleScreen> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../storage/local_storage.dart';
|
||||
import '../theme.dart';
|
||||
import 'baseline_screen.dart';
|
||||
import 'welcome_screen.dart';
|
||||
|
||||
class OnboardingScreen extends StatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
|
@ -93,16 +95,26 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_currentPage < _slides.length - 1)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _skip,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: TextStyle(color: AppColors.textTertiary),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 40),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
TextButton(
|
||||
onPressed: _showSkipForeverConfirmation,
|
||||
child: Text(
|
||||
'Skip Forever',
|
||||
style: TextStyle(color: AppColors.textTertiary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -250,6 +262,38 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
|||
MaterialPageRoute(builder: (_) => const BaselineScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSkipForeverConfirmation() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Skip Forever?'),
|
||||
content: const Text(
|
||||
'You won\'t be asked to complete the questionnaire again. '
|
||||
'You can still access the app from the About screen.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await LocalStorage.setSkipForever(true);
|
||||
Navigator.pop(dialogContext);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Skip Forever'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SlideData {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../storage/local_storage.dart';
|
||||
import '../theme.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'baseline_screen.dart';
|
||||
|
|
@ -61,7 +62,15 @@ class WelcomeScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// About link
|
||||
// Skip Forever and About links
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _showSkipForeverConfirmation(context),
|
||||
child: const Text('Skip Forever'),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
|
|
@ -69,6 +78,8 @@ class WelcomeScreen extends StatelessWidget {
|
|||
),
|
||||
child: const Text('About'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
|
|
@ -82,4 +93,38 @@ class WelcomeScreen extends StatelessWidget {
|
|||
MaterialPageRoute(builder: (_) => const BaselineScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSkipForeverConfirmation(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Skip Forever?'),
|
||||
content: const Text(
|
||||
'You won\'t be asked to complete the questionnaire again. '
|
||||
'You can still access the app from the About screen.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await LocalStorage.setSkipForever(true);
|
||||
Navigator.pop(dialogContext);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Questionnaire skipped'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Skip Forever'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,4 +270,14 @@ class LocalStorage {
|
|||
if (json == null) return false; // Default to imperial (US)
|
||||
return json['value'] as bool;
|
||||
}
|
||||
|
||||
static Future<void> setSkipForever(bool skip) async {
|
||||
await _put('skipForever', {'value': skip});
|
||||
}
|
||||
|
||||
static Future<bool> getSkipForever() async {
|
||||
final json = await _get('skipForever');
|
||||
if (json == null) return false;
|
||||
return json['value'] as bool;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import Foundation
|
|||
|
||||
import flutter_secure_storage_macos
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ flutter_launcher_icons:
|
|||
ios: true
|
||||
remove_alpha_ios: true
|
||||
image_path: "assets/icon/app_icon.png"
|
||||
adaptive_icon_background: "#FFFFFF"
|
||||
adaptive_icon_foreground: "assets/icon/app_icon_foreground.png"
|
||||
adaptive_icon_background: "#2D2D2D"
|
||||
adaptive_icon_foreground: "assets/icon/app_icon.png"
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
|||