328 lines
14 KiB
Dart
328 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class LoginScreen extends StatefulWidget {
|
|
const LoginScreen({super.key});
|
|
@override
|
|
State<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<LoginScreen> {
|
|
final _emailController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
final _nameController = TextEditingController();
|
|
final _codeController = TextEditingController();
|
|
|
|
bool _isLoginMode = true;
|
|
bool _isUnlocked = false;
|
|
bool _showInfo = false;
|
|
bool _isLoading = false;
|
|
int _tierToGrant = 1;
|
|
|
|
static const Color terminalGreen = Color(0xFFE87D25);
|
|
|
|
// --- LOGIC: REQUEST LVL 1 (FREE) ---
|
|
Future<void> _requestFreeAccess() async {
|
|
final nameController = TextEditingController();
|
|
final contactController = TextEditingController();
|
|
bool isSending = false; // Internal state for the dialog
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => StatefulBuilder(
|
|
builder: (context, setDialogState) => AlertDialog(
|
|
backgroundColor: Colors.black,
|
|
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
|
|
title: const Text("FREE_ACCESS_REQUEST", style: TextStyle(fontSize: 14)),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text("ENTER_INFO_FOR_KEY_DELIVERY:", style: TextStyle(fontSize: 10, color: Colors.white24)),
|
|
const SizedBox(height: 15),
|
|
TextField(controller: nameController, decoration: const InputDecoration(labelText: "ALIAS / NAME")),
|
|
const SizedBox(height: 10),
|
|
TextField(controller: contactController, decoration: const InputDecoration(labelText: "EMAIL_OR_CONTACT")),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: isSending ? null : () => Navigator.pop(context),
|
|
child: const Text("[ CANCEL ]", style: TextStyle(color: Colors.white24))
|
|
),
|
|
isSending
|
|
? const Padding(
|
|
padding: EdgeInsets.only(right: 20),
|
|
child: CircularProgressIndicator(color: terminalGreen, strokeWidth: 2),
|
|
)
|
|
: ElevatedButton(
|
|
onPressed: () async {
|
|
if (contactController.text.trim().isEmpty) return;
|
|
|
|
setDialogState(() => isSending = true);
|
|
|
|
try {
|
|
await FirebaseFirestore.instance.collection('requests').add({
|
|
'name': nameController.text.trim(),
|
|
'contact': contactController.text.trim(),
|
|
'timestamp': FieldValue.serverTimestamp(),
|
|
'status': 'pending',
|
|
'requestedTier': 1,
|
|
'origin': 'FREE_REQUEST'
|
|
});
|
|
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("SIGNAL_SENT: SAGE_WILL_TRANSMIT_KEY"), backgroundColor: terminalGreen)
|
|
);
|
|
} catch (e) {
|
|
setDialogState(() => isSending = false);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text("UPLINK_ERROR: $e"), backgroundColor: Colors.red)
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text("[ SEND_REQUEST ]")
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- LOGIC: VERIFY INVITE KEY ---
|
|
Future<void> _verifyInviteCode() async {
|
|
if (_codeController.text.isEmpty) return;
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final codeDoc = await FirebaseFirestore.instance.collection('invites').doc(_codeController.text.trim().toUpperCase()).get();
|
|
if (!mounted) return;
|
|
if (codeDoc.exists && codeDoc.data()?['used'] == false) {
|
|
setState(() {
|
|
_isUnlocked = true;
|
|
_tierToGrant = codeDoc.data()?['grantTier'] ?? 1;
|
|
});
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("INVALID_OR_EXPIRED_KEY"), backgroundColor: Colors.red));
|
|
}
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("SYNC_ERROR: $e")));
|
|
} finally {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
// --- LOGIC: LOGIN / REGISTER ---
|
|
Future<void> _submit() async {
|
|
if (_emailController.text.isEmpty || _passwordController.text.isEmpty) return;
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
if (_isLoginMode) {
|
|
await FirebaseAuth.instance.signInWithEmailAndPassword(email: _emailController.text.trim(), password: _passwordController.text.trim());
|
|
} else {
|
|
UserCredential userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword(email: _emailController.text.trim(), password: _passwordController.text.trim());
|
|
await FirebaseFirestore.instance.collection('invites').doc(_codeController.text.trim().toUpperCase()).update({'used': true, 'usedBy': userCredential.user!.uid});
|
|
await FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid).set({
|
|
'email': _emailController.text.trim(),
|
|
'displayName': _nameController.text.trim(),
|
|
'role': 'fan',
|
|
'tier': _tierToGrant,
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()), backgroundColor: Colors.red));
|
|
} finally {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
void _showClaimDialog() {
|
|
final claimController = TextEditingController();
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
backgroundColor: Colors.black,
|
|
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
|
|
title: const Text("CLAIM_ACCESS_KEY", style: TextStyle(fontSize: 14)),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text("ENTER_PAYPAL_EMAIL_OR_TRANS_ID:", style: TextStyle(fontSize: 10, color: Colors.white24)),
|
|
const SizedBox(height: 15),
|
|
TextField(controller: claimController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(hintText: "PAYPAL_RECEIPT_ID")),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text("[ CANCEL ]")),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
if (claimController.text.isEmpty) return;
|
|
await FirebaseFirestore.instance.collection('requests').add({
|
|
'id': claimController.text.trim(),
|
|
'timestamp': FieldValue.serverTimestamp(),
|
|
'status': 'pending',
|
|
'origin': 'PAYPAL_CLAIM'
|
|
});
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("SIGNAL_SENT: SAGE_WILL_VERIFY")));
|
|
},
|
|
child: const Text("[ SUBMIT ]")
|
|
)
|
|
],
|
|
)
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(40.0),
|
|
child: Column(
|
|
children: [
|
|
Image.asset('assets/images/logo.png', height: 100, color: terminalGreen),
|
|
const SizedBox(height: 30),
|
|
|
|
if (_showInfo) _buildTierInfo()
|
|
else if (_isLoginMode) _buildLoginFields()
|
|
else if (!_isUnlocked) _buildCodeVerifyFields()
|
|
else _buildRegistrationFields(),
|
|
|
|
const SizedBox(height: 30),
|
|
TextButton(
|
|
onPressed: () => setState(() => _showInfo = !_showInfo),
|
|
child: Text(_showInfo ? "< RETURN_TO_LOGIN" : "> VIEW_ACCESS_MATRIX",
|
|
style: const TextStyle(color: Colors.white24, fontSize: 10, letterSpacing: 2)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTierInfo() {
|
|
return Column(
|
|
children: [
|
|
const Text("--- ACCESS_LEVEL_MATRIX ---", style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 2)),
|
|
const SizedBox(height: 25),
|
|
|
|
_tierRow("01_OBSERVER", "FREE", "30s Previews / Read-Only Comms"),
|
|
const SizedBox(height: 8),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(side: const BorderSide(color: terminalGreen), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero)),
|
|
onPressed: _requestFreeAccess,
|
|
child: const Text("[ REQUEST_LVL_01 ]", style: TextStyle(fontSize: 10, color: terminalGreen)),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
_tierRow("02_COLLECTOR", "\$25/MO", "Full Archive / Active Comms"),
|
|
_paypalBtn("ACQUIRE_LVL_02", "https://www.paypal.com/ncp/payment/YOUR_L2_LINK"),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
_tierRow("03_INVESTOR", "\$100/YR", "Extractions / Private Logs / Priority"),
|
|
_paypalBtn("ACQUIRE_LVL_03", "https://www.paypal.com/ncp/payment/YOUR_L3_LINK"),
|
|
|
|
const SizedBox(height: 30),
|
|
const Text("PAID_BUT_NO_KEY?", style: TextStyle(color: Colors.white24, fontSize: 9)),
|
|
TextButton(onPressed: _showClaimDialog, child: const Text("> LOG_PAYMENT_SIGNAL", style: TextStyle(color: terminalGreen, fontSize: 10))),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _tierRow(String name, String price, String perks) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(name, style: const TextStyle(color: terminalGreen, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
Text(price, style: const TextStyle(color: Colors.white, fontSize: 12)),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(perks, style: const TextStyle(color: Colors.white38, fontSize: 9)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _paypalBtn(String label, String url) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(side: const BorderSide(color: terminalGreen), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero)),
|
|
onPressed: () async {
|
|
final Uri uri = Uri.parse(url);
|
|
if (await canLaunchUrl(uri)) await launchUrl(uri);
|
|
},
|
|
child: Text("[ $label ]", style: const TextStyle(fontSize: 10, color: terminalGreen)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoginFields() {
|
|
return Column(
|
|
children: [
|
|
const Text("[ SYSTEM_LOGIN_REQUIRED ]", style: TextStyle(letterSpacing: 2)),
|
|
const SizedBox(height: 25),
|
|
TextField(controller: _emailController, decoration: const InputDecoration(labelText: "USER_ID")),
|
|
const SizedBox(height: 10),
|
|
TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: "ENCRYPT_PASS")),
|
|
const SizedBox(height: 30),
|
|
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _submit, child: const Text("[ ENTER ]")),
|
|
TextButton(
|
|
onPressed: () => setState(() => _isLoginMode = false),
|
|
child: const Text("> INITIALIZE_NEW_KEY", style: TextStyle(color: Colors.white24, fontSize: 10))
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCodeVerifyFields() {
|
|
return Column(
|
|
children: [
|
|
const Text("[ KEY_VALIDATION ]", style: TextStyle(letterSpacing: 2)),
|
|
const SizedBox(height: 25),
|
|
TextField(
|
|
controller: _codeController,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(letterSpacing: 8, color: Colors.white, fontWeight: FontWeight.bold),
|
|
decoration: const InputDecoration(hintText: "ENTER_KEY", hintStyle: TextStyle(color: Colors.white10))
|
|
),
|
|
const SizedBox(height: 30),
|
|
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _verifyInviteCode, child: const Text("[ VALIDATE ]")),
|
|
TextButton(onPressed: () => setState(() => _isLoginMode = true), child: const Text("> BACK", style: TextStyle(color: Colors.white24, fontSize: 10))),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRegistrationFields() {
|
|
return Column(
|
|
children: [
|
|
Text("[ LVL_0${_tierToGrant}_ACCESS_GRANTED ]", style: const TextStyle(color: terminalGreen, letterSpacing: 2)),
|
|
const SizedBox(height: 25),
|
|
TextField(controller: _nameController, decoration: const InputDecoration(labelText: "ALIAS / NAME")),
|
|
const SizedBox(height: 10),
|
|
TextField(controller: _emailController, decoration: const InputDecoration(labelText: "EMAIL_ADDR")),
|
|
const SizedBox(height: 10),
|
|
TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: "CREATE_PASS")),
|
|
const SizedBox(height: 30),
|
|
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _submit, child: const Text("[ INITIALIZE_PROFILE ]")),
|
|
],
|
|
);
|
|
}
|
|
} |