mobile_nebula/lib/screens/EnrollmentScreen.dart
Ian VanSchooten 21d8265f42
Clean up enrollment screen (#234)
## Android

Blank form:
|Before|After|
|--|--|
|<img width="395" alt="image" src="https://github.com/user-attachments/assets/5c8a8e26-d030-4b14-b91a-9cbf5e1b2e50" />|<img width="393" alt="Android Studio 2025-01-24 11 34 32" src="https://github.com/user-attachments/assets/408c8003-9bc0-4e09-af58-44f5728dcb59" />|

Text entered:
|Before|After|
|--|--|
|<img width="397" alt="Arc 2025-01-24 11 47 19" src="https://github.com/user-attachments/assets/d5e8bc70-1786-488c-aa1c-c45ddf9a92de" />|<img width="393" alt="image" src="https://github.com/user-attachments/assets/5765e9c3-cf50-4913-b4d1-78780918f35d" />|

Submitted empty:
|Before|After|
|--|--|
|<img width="395" alt="image" src="https://github.com/user-attachments/assets/37b8b9c8-760f-479e-b39f-3502367567ec" />|<img width="397" alt="Android Studio 2025-01-24 11 34 47" src="https://github.com/user-attachments/assets/c381725e-f449-43de-961f-768205b60028" />|

Submitted invalid:
|Before|After|
|--|--|
|<img width="388" alt="image" src="https://github.com/user-attachments/assets/3a3859e1-eff7-4111-9dd0-15edbd3bb998" />|<img width="389" alt="image" src="https://github.com/user-attachments/assets/1ab811df-e3ff-4a1f-9d3d-a2869cbc1877" />|


Unchanged:
<img width="388" alt="image" src="https://github.com/user-attachments/assets/f2719677-a73f-435d-b3c6-a0a4fd64759b" />


## iOS


Blank form:
|Before|After|
|--|--|
|<img width="454" alt="image" src="https://github.com/user-attachments/assets/9afed9d1-c63b-4fcf-80a8-c2a9d2c18e76" />|<img width="454" alt="image" src="https://github.com/user-attachments/assets/c185b7f1-a2b3-4e9b-a49f-fb9790214c3a" />|

Text entered:
|Before|After|
|--|--|
|<img width="452" alt="image" src="https://github.com/user-attachments/assets/d3be265d-9076-4a19-865b-413dfad79ed4" />|<img width="466" alt="image" src="https://github.com/user-attachments/assets/c23e99eb-1001-4f80-b88f-62ffa8c8f7dd" />|


Submitted empty:
(note it says invalid before)
|Before|After|
|--|--|
|<img width="463" alt="image" src="https://github.com/user-attachments/assets/e26c0642-e6cc-4e97-885d-b0309e58ace6" />|<img width="455" alt="image" src="https://github.com/user-attachments/assets/95b8f975-41b5-48a0-b75a-9cb6a9acc50e" />|

Submitted invalid:
|Before|After|
|--|--|
|<img width="484" alt="image" src="https://github.com/user-attachments/assets/1daf3c2e-5fbb-477c-b0c7-66689170d97a" />|<img width="463" alt="image" src="https://github.com/user-attachments/assets/8bef248f-a607-4373-9e9e-6ff08f402b4a" />|
2025-01-24 14:22:40 -05:00

214 lines
6.6 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/buttons/PrimaryButton.dart';
import 'package:url_launcher/url_launcher.dart';
import '../components/config/ConfigItem.dart';
import '../components/config/ConfigSection.dart';
class EnrollmentScreen extends StatefulWidget {
final String? code;
final StreamController? stream;
final bool allowCodeEntry;
static const routeName = '/v1/mobile-enrollment';
// Attempts to find an enrollment code in the provided url. If one is not found then assume the input was
// an enrollment code. Primarily to support manual dn enrollment where the user can input a code or a url.
static String parseCode(String url) {
final uri = Uri.parse(url);
if (uri.path != EnrollmentScreen.routeName) {
return url;
}
if (uri.hasFragment) {
final qp = Uri.splitQueryString(uri.fragment);
return qp["code"] ?? "";
}
return url;
}
const EnrollmentScreen({super.key, this.code, this.stream, this.allowCodeEntry = false});
@override
_EnrollmentScreenState createState() => _EnrollmentScreenState();
}
class _EnrollmentScreenState extends State<EnrollmentScreen> {
String? error;
var enrolled = false;
var enrollInput = TextEditingController();
String? code;
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
void initState() {
code = widget.code;
super.initState();
_enroll();
}
@override
void dispose() {
enrollInput.dispose();
super.dispose();
}
_enroll() async {
try {
await platform.invokeMethod("dn.enroll", code);
setState(() {
enrolled = true;
if (widget.stream != null) {
// Signal a new site has been added
widget.stream!.add(null);
}
});
} on PlatformException catch (err) {
setState(() {
error = err.details ?? err.message;
});
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final bodyTextStyle = textTheme.bodyLarge!.apply(color: colorScheme.onPrimary);
final contactUri = Uri.parse('mailto:support@defined.net');
Widget child;
AlignmentGeometry? alignment;
if (code == null) {
if (widget.allowCodeEntry) {
child = _codeEntry();
} else {
// No code, show the error
child = Padding(
child: Center(
child: Text(
'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.',
textAlign: TextAlign.center,
)),
padding: EdgeInsets.only(top: 20));
}
} else if (this.error != null) {
// Error while enrolling, display it
child = Center(
child: Column(
children: [
Padding(
child: SelectableText(
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.'),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20)),
Padding(
child: SelectableText.rich(TextSpan(children: [
TextSpan(text: 'If the problem persists, please let us know at '),
TextSpan(
text: 'support@defined.net',
style: bodyTextStyle.apply(color: colorScheme.primary),
recognizer: TapGestureRecognizer()
..onTap = () async {
if (await canLaunchUrl(contactUri)) {
print(await launchUrl(contactUri));
}
},
),
TextSpan(text: ' and provide the following error:'),
])),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10)),
Container(
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(16)),
color: Theme.of(context).colorScheme.errorContainer,
),
],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
));
} else if (this.enrolled) {
// Enrollment complete!
child = Padding(
child: Center(
child: Text(
'Enrollment complete! 🎉',
textAlign: TextAlign.center,
)),
padding: EdgeInsets.only(top: 20));
} else {
// Have a code and actively enrolling
alignment = Alignment.center;
child = Center(
child: Column(children: [
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)),
PlatformCircularProgressIndicator(cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 50);
})
]));
}
return SimplePage(title: Text('Enroll with Managed Nebula'), child: child, alignment: alignment);
}
Widget _codeEntry() {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String? validator(String? value) {
if (value == null || value.isEmpty) {
return 'Code or link is required';
}
return null;
}
Future<void> onSubmit() async {
final bool isValid = _formKey.currentState?.validate() ?? false;
if (!isValid) {
return;
}
setState(() {
code = EnrollmentScreen.parseCode(enrollInput.text);
error = null;
_enroll();
});
}
final input = Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: PlatformTextFormField(
controller: enrollInput,
validator: validator,
hintText: 'from admin.defined.net',
cupertino: (_, __) => CupertinoTextFormFieldData(
prefix: Text("Code or link"),
),
material: (_, __) => MaterialTextFormFieldData(
decoration: const InputDecoration(labelText: 'Code or link'),
),
));
final form = Form(
key: _formKey,
child: Platform.isAndroid ? input : ConfigSection(children: [input]),
);
return Column(children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: form,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(children: [Expanded(child: PrimaryButton(child: Text('Submit'), onPressed: onSubmit))]))
]);
}
}