1101 lines
35 KiB
Dart
1101 lines
35 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import '../../core/providers/auth_provider.dart';
|
||
import '../../core/providers/locale_provider.dart';
|
||
import '../../core/router/app_router.dart';
|
||
import '../../core/theme/app_theme.dart';
|
||
import '../../core/widgets/tooth_logo.dart';
|
||
import 'auth_widgets.dart';
|
||
|
||
class WelcomePricingScreen extends ConsumerStatefulWidget {
|
||
const WelcomePricingScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<WelcomePricingScreen> createState() =>
|
||
_WelcomePricingScreenState();
|
||
}
|
||
|
||
class _WelcomePricingScreenState extends ConsumerState<WelcomePricingScreen> {
|
||
bool _yearly = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final locale = ref.watch(localeProvider);
|
||
final copy = _WelcomeCopy.of(locale.languageCode);
|
||
final auth = ref.watch(authProvider);
|
||
final isDesktop = MediaQuery.sizeOf(context).width > 980;
|
||
|
||
return Scaffold(
|
||
backgroundColor: AppColors.background,
|
||
body: Stack(
|
||
children: [
|
||
if (isDesktop)
|
||
Row(
|
||
children: [
|
||
Expanded(child: _HeroPane(copy: copy, compact: false)),
|
||
Expanded(child: _ContentPane(copy: copy, yearly: _yearly)),
|
||
],
|
||
)
|
||
else
|
||
SafeArea(
|
||
child: CustomScrollView(
|
||
slivers: [
|
||
SliverToBoxAdapter(
|
||
child: _HeroPane(copy: copy, compact: true)),
|
||
SliverToBoxAdapter(
|
||
child: _ContentPane(copy: copy, yearly: _yearly),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Positioned(
|
||
top: MediaQuery.paddingOf(context).top + 12,
|
||
right: 12,
|
||
child: Wrap(
|
||
spacing: 8,
|
||
children: [
|
||
if (auth.isAuthenticated && auth.activeTenant != null)
|
||
OutlinedButton.icon(
|
||
onPressed: () => context.go(
|
||
auth.activeTenant!.tenant.isLab
|
||
? routeLabDashboard
|
||
: routeClinicDashboard,
|
||
),
|
||
icon: const Icon(Icons.arrow_back_rounded, size: 18),
|
||
label: Text(copy.backToApp),
|
||
style: OutlinedButton.styleFrom(
|
||
backgroundColor: Colors.white.withValues(alpha: 0.92),
|
||
foregroundColor: AppColors.textPrimary,
|
||
side: const BorderSide(color: AppColors.border),
|
||
),
|
||
),
|
||
_LanguageFab(locale: locale),
|
||
],
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
child: _BottomActionBar(
|
||
copy: copy,
|
||
onYearlyChanged: (value) => setState(() => _yearly = value),
|
||
yearly: _yearly,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _HeroPane extends StatelessWidget {
|
||
const _HeroPane({required this.copy, required this.compact});
|
||
|
||
final _WelcomeCopy copy;
|
||
final bool compact;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
constraints: BoxConstraints(minHeight: compact ? 360 : double.infinity),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [Color(0xFF08111F), Color(0xFF0E325F), Color(0xFF13639C)],
|
||
),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
const AnimatedAuthBg(bright: true),
|
||
Padding(
|
||
padding: EdgeInsets.fromLTRB(
|
||
compact ? 24 : 48,
|
||
compact ? 32 : 64,
|
||
compact ? 24 : 48,
|
||
compact ? 36 : 64,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: compact ? 60 : 72,
|
||
height: compact ? 60 : 72,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.14),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(
|
||
color: Colors.white.withValues(alpha: 0.18),
|
||
),
|
||
),
|
||
child: Center(
|
||
child: ToothLogo(
|
||
size: compact ? 30 : 36,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
Text(
|
||
'DLS',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: compact ? 34 : 48,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: 1.4,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Text(
|
||
copy.heroTitle,
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: compact ? 28 : 42,
|
||
fontWeight: FontWeight.w800,
|
||
height: 1.08,
|
||
letterSpacing: -0.8,
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
Text(
|
||
copy.heroSubtitle,
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.72),
|
||
fontSize: compact ? 14 : 17,
|
||
height: 1.6,
|
||
),
|
||
),
|
||
const SizedBox(height: 28),
|
||
Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: [
|
||
_HeroMetric(
|
||
label: copy.metricClinics,
|
||
value: copy.metricClinicsValue,
|
||
),
|
||
_HeroMetric(
|
||
label: copy.metricSpeed,
|
||
value: copy.metricSpeedValue,
|
||
),
|
||
_HeroMetric(
|
||
label: copy.metricAi,
|
||
value: copy.metricAiValue,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 28),
|
||
...copy.heroBullets.map(
|
||
(item) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 2),
|
||
width: 22,
|
||
height: 22,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.14),
|
||
borderRadius: BorderRadius.circular(999),
|
||
),
|
||
child: const Icon(
|
||
Icons.check_rounded,
|
||
size: 14,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(
|
||
item,
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.92),
|
||
fontSize: compact ? 14 : 15,
|
||
height: 1.45,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ContentPane extends StatelessWidget {
|
||
const _ContentPane({required this.copy, required this.yearly});
|
||
|
||
final _WelcomeCopy copy;
|
||
final bool yearly;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final plans = copy.buildPlans(yearly);
|
||
|
||
return Padding(
|
||
padding: EdgeInsets.fromLTRB(
|
||
24,
|
||
24,
|
||
24,
|
||
MediaQuery.of(context).padding.bottom + 110,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
copy.packageEyebrow,
|
||
style: const TextStyle(
|
||
color: AppColors.accent,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
copy.packageTitle,
|
||
style: const TextStyle(
|
||
fontSize: 30,
|
||
fontWeight: FontWeight.w800,
|
||
color: AppColors.textPrimary,
|
||
letterSpacing: -0.7,
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
copy.packageSubtitle,
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
color: AppColors.textSecondary,
|
||
height: 1.6,
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final wide = constraints.maxWidth > 720;
|
||
return Wrap(
|
||
spacing: 16,
|
||
runSpacing: 16,
|
||
children: plans
|
||
.map(
|
||
(plan) => SizedBox(
|
||
width: wide ? (constraints.maxWidth - 16) / 2 : null,
|
||
child: _PlanCard(plan: plan, yearly: yearly),
|
||
),
|
||
)
|
||
.toList(),
|
||
);
|
||
},
|
||
),
|
||
const SizedBox(height: 28),
|
||
_FreeUsageCard(copy: copy),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _BottomActionBar extends StatelessWidget {
|
||
const _BottomActionBar({
|
||
required this.copy,
|
||
required this.yearly,
|
||
required this.onYearlyChanged,
|
||
});
|
||
|
||
final _WelcomeCopy copy;
|
||
final bool yearly;
|
||
final ValueChanged<bool> onYearlyChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: EdgeInsets.fromLTRB(
|
||
16,
|
||
14,
|
||
16,
|
||
MediaQuery.paddingOf(context).bottom + 14,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.96),
|
||
border: const Border(top: BorderSide(color: AppColors.border)),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.08),
|
||
blurRadius: 18,
|
||
offset: const Offset(0, -8),
|
||
),
|
||
],
|
||
),
|
||
child: SafeArea(
|
||
top: false,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Container(
|
||
height: 48,
|
||
padding: const EdgeInsets.all(4),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.surface,
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: AppColors.border),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: _BillingOptionButton(
|
||
label: copy.monthly,
|
||
selected: !yearly,
|
||
onTap: () => onYearlyChanged(false),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: _BillingOptionButton(
|
||
label: copy.yearly,
|
||
caption: copy.yearlyDiscount,
|
||
selected: yearly,
|
||
onTap: () => onYearlyChanged(true),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: FilledButton(
|
||
onPressed: () => context.go(routeSignUp),
|
||
style: FilledButton.styleFrom(
|
||
minimumSize: const Size.fromHeight(48),
|
||
backgroundColor: AppColors.primary,
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
),
|
||
child: Text(copy.startNow),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
OutlinedButton(
|
||
onPressed: () => context.go(routeSignIn),
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size(112, 48),
|
||
foregroundColor: AppColors.textPrimary,
|
||
side: const BorderSide(color: AppColors.border),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
),
|
||
child: Text(copy.signIn),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _BillingOptionButton extends StatelessWidget {
|
||
const _BillingOptionButton({
|
||
required this.label,
|
||
required this.selected,
|
||
required this.onTap,
|
||
this.caption,
|
||
});
|
||
|
||
final String label;
|
||
final String? caption;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return InkWell(
|
||
borderRadius: BorderRadius.circular(10),
|
||
onTap: onTap,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 180),
|
||
decoration: BoxDecoration(
|
||
color: selected ? Colors.white : Colors.transparent,
|
||
borderRadius: BorderRadius.circular(10),
|
||
boxShadow: selected
|
||
? [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.05),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w700,
|
||
color:
|
||
selected ? AppColors.textPrimary : AppColors.textSecondary,
|
||
),
|
||
),
|
||
if (caption != null)
|
||
Text(
|
||
caption!,
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
color: AppColors.success,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PlanCard extends StatelessWidget {
|
||
const _PlanCard({required this.plan, required this.yearly});
|
||
|
||
final _PlanViewModel plan;
|
||
final bool yearly;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: plan.highlighted ? const Color(0xFFF8FBFF) : Colors.white,
|
||
borderRadius: BorderRadius.circular(24),
|
||
border: Border.all(
|
||
color: plan.highlighted ? const Color(0xFF93C5FD) : AppColors.border,
|
||
width: plan.highlighted ? 1.4 : 1,
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.04),
|
||
blurRadius: 18,
|
||
offset: const Offset(0, 10),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
plan.name,
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
color: AppColors.textPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
plan.subtitle,
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: AppColors.textSecondary,
|
||
height: 1.45,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (plan.badge != null)
|
||
Container(
|
||
padding:
|
||
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFE0F2FE),
|
||
borderRadius: BorderRadius.circular(999),
|
||
),
|
||
child: Text(
|
||
plan.badge!,
|
||
style: const TextStyle(
|
||
color: AppColors.accent,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 18),
|
||
RichText(
|
||
text: TextSpan(
|
||
children: [
|
||
TextSpan(
|
||
text: plan.price,
|
||
style: const TextStyle(
|
||
fontSize: 34,
|
||
fontWeight: FontWeight.w900,
|
||
color: AppColors.textPrimary,
|
||
letterSpacing: -1,
|
||
),
|
||
),
|
||
TextSpan(
|
||
text: plan.period,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
yearly ? plan.yearlyNote : plan.monthlyNote,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_FeatureRow(
|
||
icon: Icons.auto_awesome_rounded,
|
||
text: plan.aiCredits,
|
||
emphasized: true,
|
||
),
|
||
const SizedBox(height: 8),
|
||
...plan.features.map(
|
||
(feature) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: _FeatureRow(
|
||
icon: Icons.check_circle_outline_rounded,
|
||
text: feature,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: OutlinedButton(
|
||
onPressed: () => context.go(routeSignUp),
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size.fromHeight(46),
|
||
foregroundColor: plan.highlighted
|
||
? AppColors.primary
|
||
: AppColors.textPrimary,
|
||
side: BorderSide(
|
||
color:
|
||
plan.highlighted ? AppColors.primary : AppColors.border,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
),
|
||
child: Text(plan.cta),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _FeatureRow extends StatelessWidget {
|
||
const _FeatureRow({
|
||
required this.icon,
|
||
required this.text,
|
||
this.emphasized = false,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String text;
|
||
final bool emphasized;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(
|
||
icon,
|
||
size: 18,
|
||
color: emphasized ? const Color(0xFF7C3AED) : AppColors.success,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
text,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: emphasized ? FontWeight.w700 : FontWeight.w500,
|
||
color:
|
||
emphasized ? AppColors.textPrimary : AppColors.textSecondary,
|
||
height: 1.45,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _FreeUsageCard extends StatelessWidget {
|
||
const _FreeUsageCard({required this.copy});
|
||
|
||
final _WelcomeCopy copy;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF8FAFC),
|
||
borderRadius: BorderRadius.circular(24),
|
||
border: Border.all(color: AppColors.border),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
copy.freeUsageTitle,
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
color: AppColors.textPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
copy.freeUsageSubtitle,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
color: AppColors.textSecondary,
|
||
height: 1.55,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
...copy.freeUsageBullets.map(
|
||
(item) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: _FeatureRow(
|
||
icon: Icons.lightbulb_outline_rounded,
|
||
text: item,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _HeroMetric extends StatelessWidget {
|
||
const _HeroMetric({required this.label, required this.value});
|
||
|
||
final String label;
|
||
final String value;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
value,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: Colors.white.withValues(alpha: 0.72),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LanguageFab extends ConsumerWidget {
|
||
const _LanguageFab({required this.locale});
|
||
|
||
final Locale locale;
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
const options = [
|
||
('tr', 'TR'),
|
||
('en', 'EN'),
|
||
('de', 'DE'),
|
||
('ru', 'RU'),
|
||
('ar', 'AR'),
|
||
];
|
||
|
||
return PopupMenuButton<String>(
|
||
tooltip: 'Language',
|
||
onSelected: (value) =>
|
||
ref.read(localeProvider.notifier).setLocale(Locale(value)),
|
||
itemBuilder: (_) => options
|
||
.map(
|
||
(option) => PopupMenuItem<String>(
|
||
value: option.$1,
|
||
child: Row(
|
||
children: [
|
||
Expanded(child: Text(option.$2)),
|
||
if (locale.languageCode == option.$1)
|
||
const Icon(Icons.check_rounded, size: 16),
|
||
],
|
||
),
|
||
),
|
||
)
|
||
.toList(),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.92),
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: AppColors.border),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Icon(Icons.language_rounded, size: 18),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
locale.languageCode.toUpperCase(),
|
||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PlanViewModel {
|
||
const _PlanViewModel({
|
||
required this.name,
|
||
required this.subtitle,
|
||
required this.price,
|
||
required this.period,
|
||
required this.monthlyNote,
|
||
required this.yearlyNote,
|
||
required this.aiCredits,
|
||
required this.features,
|
||
required this.cta,
|
||
this.badge,
|
||
this.highlighted = false,
|
||
});
|
||
|
||
final String name;
|
||
final String subtitle;
|
||
final String price;
|
||
final String period;
|
||
final String monthlyNote;
|
||
final String yearlyNote;
|
||
final String aiCredits;
|
||
final List<String> features;
|
||
final String cta;
|
||
final String? badge;
|
||
final bool highlighted;
|
||
}
|
||
|
||
class _WelcomeCopy {
|
||
const _WelcomeCopy({
|
||
required this.heroTitle,
|
||
required this.heroSubtitle,
|
||
required this.heroBullets,
|
||
required this.metricClinics,
|
||
required this.metricClinicsValue,
|
||
required this.metricSpeed,
|
||
required this.metricSpeedValue,
|
||
required this.metricAi,
|
||
required this.metricAiValue,
|
||
required this.packageEyebrow,
|
||
required this.packageTitle,
|
||
required this.packageSubtitle,
|
||
required this.monthly,
|
||
required this.yearly,
|
||
required this.yearlyDiscount,
|
||
required this.startNow,
|
||
required this.signIn,
|
||
required this.backToApp,
|
||
required this.freeUsageTitle,
|
||
required this.freeUsageSubtitle,
|
||
required this.freeUsageBullets,
|
||
required this.planFree,
|
||
required this.planFreeSub,
|
||
required this.planStarter,
|
||
required this.planStarterSub,
|
||
required this.planPro,
|
||
required this.planProSub,
|
||
required this.planEnterprise,
|
||
required this.planEnterpriseSub,
|
||
required this.freeCta,
|
||
required this.paidCta,
|
||
required this.enterpriseCta,
|
||
required this.recommendedBadge,
|
||
required this.enterpriseBadge,
|
||
required this.perMonth,
|
||
required this.perYear,
|
||
required this.customPrice,
|
||
required this.noCardTrial,
|
||
required this.annualPrepay,
|
||
required this.customContact,
|
||
});
|
||
|
||
final String heroTitle;
|
||
final String heroSubtitle;
|
||
final List<String> heroBullets;
|
||
final String metricClinics;
|
||
final String metricClinicsValue;
|
||
final String metricSpeed;
|
||
final String metricSpeedValue;
|
||
final String metricAi;
|
||
final String metricAiValue;
|
||
final String packageEyebrow;
|
||
final String packageTitle;
|
||
final String packageSubtitle;
|
||
final String monthly;
|
||
final String yearly;
|
||
final String yearlyDiscount;
|
||
final String startNow;
|
||
final String signIn;
|
||
final String backToApp;
|
||
final String freeUsageTitle;
|
||
final String freeUsageSubtitle;
|
||
final List<String> freeUsageBullets;
|
||
final String planFree;
|
||
final String planFreeSub;
|
||
final String planStarter;
|
||
final String planStarterSub;
|
||
final String planPro;
|
||
final String planProSub;
|
||
final String planEnterprise;
|
||
final String planEnterpriseSub;
|
||
final String freeCta;
|
||
final String paidCta;
|
||
final String enterpriseCta;
|
||
final String recommendedBadge;
|
||
final String enterpriseBadge;
|
||
final String perMonth;
|
||
final String perYear;
|
||
final String customPrice;
|
||
final String noCardTrial;
|
||
final String annualPrepay;
|
||
final String customContact;
|
||
|
||
static _WelcomeCopy of(String code) {
|
||
switch (code) {
|
||
case 'tr':
|
||
return const _WelcomeCopy(
|
||
heroTitle: 'Klinik ve laboratuvar operasyonunu tek akışta yönetin.',
|
||
heroSubtitle:
|
||
'İş takibi, finans, bağlantılar ve AI destekli operasyon yardımı tek uygulamada birleşir.',
|
||
heroBullets: [
|
||
'Klinik ve laboratuvar arasında iş akışı ve teslim süreçleri aynı dilde ilerler.',
|
||
'Hasta, ürün, fiyat ve onay süreçleri gerçek saha kullanımına göre ölçeklenir.',
|
||
'AI özellikleri kredi bazlı ilerleyecek şekilde ürünleştirilmeye hazırdır.',
|
||
],
|
||
metricClinics: 'Kurulum',
|
||
metricClinicsValue: 'Aynı gün',
|
||
metricSpeed: 'Odak',
|
||
metricSpeedValue: 'Operasyon + finans',
|
||
metricAi: 'AI yaklaşımı',
|
||
metricAiValue: 'Kredi bazlı',
|
||
packageEyebrow: 'Paketler',
|
||
packageTitle: 'Trial ve paket yapısını şimdiden net gösterelim.',
|
||
packageSubtitle:
|
||
'Bu ekran şimdilik tanıtım ve yönlendirme amaçlıdır. Paketler aylık ve yıllık olarak ayrıldı; AI kredi mantığı da görünür halde.',
|
||
monthly: 'Aylık',
|
||
yearly: 'Yıllık',
|
||
yearlyDiscount: '%20 avantaj',
|
||
startNow: 'Hemen Başla',
|
||
signIn: 'Giriş Yap',
|
||
backToApp: 'Uygulamaya dön',
|
||
freeUsageTitle: 'Ücretsiz kullanım için önerilen model',
|
||
freeUsageSubtitle:
|
||
'Monetization başlamadan önce ücretsiz katmanı ürün denemesi için güçlü ama kontrollü tutmak en sağlıklı yapı olur.',
|
||
freeUsageBullets: [
|
||
'Süresiz ücretsiz plan: 1 tenant, sınırlı ekip üyesi ve aylık düşük AI kredi.',
|
||
'Kart istemeyen 14 günlük Pro deneme: kullanıcı ilk değeri hızlı görür, bariyer düşer.',
|
||
'Referans veya ilk aktivasyon sonrası bonus kredi: AI özelliğini tattırır.',
|
||
],
|
||
planFree: 'Free',
|
||
planFreeSub: 'Temel operasyonu görmek isteyenler için',
|
||
planStarter: 'Starter',
|
||
planStarterSub: 'Yeni başlayan klinik ve laboratuvarlar için',
|
||
planPro: 'Pro',
|
||
planProSub: 'Düzenli iş trafiği ve ekip yönetimi için',
|
||
planEnterprise: 'Enterprise',
|
||
planEnterpriseSub: 'Çoklu şube, operasyon ve özel süreçler için',
|
||
freeCta: 'Ücretsiz başla',
|
||
paidCta: 'Denemeyi başlat',
|
||
enterpriseCta: 'Görüşme planla',
|
||
recommendedBadge: 'Önerilen',
|
||
enterpriseBadge: 'Kurumsal',
|
||
perMonth: '/ ay',
|
||
perYear: '/ yıl',
|
||
customPrice: 'Özel',
|
||
noCardTrial: 'Kart gerektirmeyen başlangıç',
|
||
annualPrepay: 'Yıllık peşin ödeme ile daha avantajlı',
|
||
customContact: 'Özel AI kredi ve tenant yapısı planlanır',
|
||
);
|
||
default:
|
||
return const _WelcomeCopy(
|
||
heroTitle: 'Run clinic and lab operations in one shared workflow.',
|
||
heroSubtitle:
|
||
'Jobs, finance, connections, and AI-assisted operations live in the same product surface.',
|
||
heroBullets: [
|
||
'Clinics and labs follow the same production language and handoff flow.',
|
||
'Patients, products, pricing, and approvals scale with real field usage.',
|
||
'AI features are ready to evolve into a credit-based monetization layer.',
|
||
],
|
||
metricClinics: 'Setup',
|
||
metricClinicsValue: 'Same day',
|
||
metricSpeed: 'Focus',
|
||
metricSpeedValue: 'Ops + finance',
|
||
metricAi: 'AI model',
|
||
metricAiValue: 'Credit-based',
|
||
packageEyebrow: 'Plans',
|
||
packageTitle:
|
||
'Make trial and package structure visible from day one.',
|
||
packageSubtitle:
|
||
'This screen is intentionally promotional for now. Plans are split monthly and yearly, and AI credits are visible for future packaging.',
|
||
monthly: 'Monthly',
|
||
yearly: 'Yearly',
|
||
yearlyDiscount: 'Save 20%',
|
||
startNow: 'Get Started',
|
||
signIn: 'Sign In',
|
||
backToApp: 'Back to app',
|
||
freeUsageTitle: 'Recommended free usage policy',
|
||
freeUsageSubtitle:
|
||
'Before full monetization, a controlled free tier is the healthiest way to drive adoption without hurting conversion.',
|
||
freeUsageBullets: [
|
||
'Free forever tier with 1 tenant, limited team seats, and low monthly AI credits.',
|
||
'14-day Pro trial without requiring a card to reduce onboarding friction.',
|
||
'Bonus credits after referral or first activation to showcase AI value.',
|
||
],
|
||
planFree: 'Free',
|
||
planFreeSub: 'For teams exploring the product',
|
||
planStarter: 'Starter',
|
||
planStarterSub: 'For early-stage clinics and labs',
|
||
planPro: 'Pro',
|
||
planProSub: 'For active teams with recurring job volume',
|
||
planEnterprise: 'Enterprise',
|
||
planEnterpriseSub: 'For multi-branch and custom operations',
|
||
freeCta: 'Start free',
|
||
paidCta: 'Start trial',
|
||
enterpriseCta: 'Book a demo',
|
||
recommendedBadge: 'Recommended',
|
||
enterpriseBadge: 'Enterprise',
|
||
perMonth: '/ mo',
|
||
perYear: '/ yr',
|
||
customPrice: 'Custom',
|
||
noCardTrial: 'No-card onboarding',
|
||
annualPrepay: 'Lower effective cost with annual billing',
|
||
customContact: 'Custom AI credits and tenant structure',
|
||
);
|
||
}
|
||
}
|
||
|
||
List<_PlanViewModel> buildPlans(bool yearly) => [
|
||
_PlanViewModel(
|
||
name: planFree,
|
||
subtitle: planFreeSub,
|
||
price: yearly ? '0₺' : '0₺',
|
||
period: yearly ? perYear : perMonth,
|
||
monthlyNote: noCardTrial,
|
||
yearlyNote: noCardTrial,
|
||
aiCredits: yearly ? '300 AI kredi / yıl' : '25 AI kredi / ay',
|
||
features: const [
|
||
'1 tenant / temel ekip kullanımı',
|
||
'İş ve hasta akışını deneme',
|
||
'Sınırlı rapor ve temel finans görünümü',
|
||
],
|
||
cta: freeCta,
|
||
),
|
||
_PlanViewModel(
|
||
name: planStarter,
|
||
subtitle: planStarterSub,
|
||
price: yearly ? '7.680₺' : '800₺',
|
||
period: yearly ? perYear : perMonth,
|
||
monthlyNote: noCardTrial,
|
||
yearlyNote: annualPrepay,
|
||
aiCredits: yearly ? '3.600 AI kredi / yıl' : '300 AI kredi / ay',
|
||
features: const [
|
||
'Çoklu kullanıcı ve temel tenant yönetimi',
|
||
'İş akışı, ürün ve bağlantı yönetimi',
|
||
'Temel AI yardımcı deneyimi',
|
||
],
|
||
cta: paidCta,
|
||
),
|
||
_PlanViewModel(
|
||
name: planPro,
|
||
subtitle: planProSub,
|
||
price: yearly ? '17.280₺' : '1.800₺',
|
||
period: yearly ? perYear : perMonth,
|
||
monthlyNote: noCardTrial,
|
||
yearlyNote: annualPrepay,
|
||
aiCredits: yearly ? '14.400 AI kredi / yıl' : '1.200 AI kredi / ay',
|
||
features: const [
|
||
'Gelişmiş finans ve fiyatlandırma görünürlüğü',
|
||
'Daha yüksek ekip ve tenant esnekliği',
|
||
'Öncelikli AI kullanım ve operasyon desteği',
|
||
],
|
||
cta: paidCta,
|
||
badge: recommendedBadge,
|
||
highlighted: true,
|
||
),
|
||
_PlanViewModel(
|
||
name: planEnterprise,
|
||
subtitle: planEnterpriseSub,
|
||
price: customPrice,
|
||
period: '',
|
||
monthlyNote: customContact,
|
||
yearlyNote: customContact,
|
||
aiCredits: 'Özel AI kredi havuzu ve kurallar',
|
||
features: const [
|
||
'Super admin, çoklu tenant ve özel onboarding',
|
||
'Saha sürecine göre özelleşen workflow yapısı',
|
||
'Kurumsal SLA, entegrasyon ve destek',
|
||
],
|
||
cta: enterpriseCta,
|
||
badge: enterpriseBadge,
|
||
),
|
||
];
|
||
}
|