diff --git a/flutter-ory-network/lib/blocs/registration/registration_bloc.dart b/flutter-ory-network/lib/blocs/registration/registration_bloc.dart index 97cc1729..6d37bc73 100644 --- a/flutter-ory-network/lib/blocs/registration/registration_bloc.dart +++ b/flutter-ory-network/lib/blocs/registration/registration_bloc.dart @@ -98,13 +98,13 @@ class RegistrationBloc extends Bloc { try { if (state.registrationFlow != null) { emit(state.copyWith(isLoading: true, message: null)); - await repository.updateRegistrationFlow( + final session = await repository.updateRegistrationFlow( flowId: state.registrationFlow!.id, group: event.group, name: event.name, value: event.value, nodes: state.registrationFlow!.ui.nodes.toList()); - authBloc.add(ChangeAuthStatus(status: AuthStatus.authenticated)); + authBloc.add(AddSession(session: session)); } } on BadRequestException catch (e) { emit(state.copyWith(registrationFlow: e.flow, isLoading: false)); diff --git a/flutter-ory-network/lib/pages/login.dart b/flutter-ory-network/lib/pages/login.dart index e5790cd9..5a79aadc 100644 --- a/flutter-ory-network/lib/pages/login.dart +++ b/flutter-ory-network/lib/pages/login.dart @@ -1,8 +1,6 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ory_client/ory_client.dart'; @@ -52,47 +50,23 @@ class LoginForm extends StatelessWidget { final nodes = state.loginFlow!.ui.nodes; // get default nodes from all nodes - final defaultNodes = nodes.where((node) { - if (node.group == UiNodeGroupEnum.default_) { - if (node.attributes.oneOf.isType(UiNodeInputAttributes)) { - final attributes = - node.attributes.oneOf.value as UiNodeInputAttributes; - if (attributes.type == UiNodeInputAttributesTypeEnum.hidden) { - return false; - } else { - return true; - } - } - } - return false; - }).toList(); + final defaultNodes = getNodesOfGroup(UiNodeGroupEnum.default_, nodes); + + // get code nodes from all nodes + final codeNodes = getNodesOfGroup(UiNodeGroupEnum.code, nodes); // get password nodes from all nodes - final passwordNodes = - nodes.where((node) => node.group == UiNodeGroupEnum.password).toList(); + final passwordNodes = getNodesOfGroup(UiNodeGroupEnum.password, nodes); // get lookup secret nodes from all nodes - final lookupSecretNodes = nodes - .where((node) => node.group == UiNodeGroupEnum.lookupSecret) - .toList(); + final lookupSecretNodes = + getNodesOfGroup(UiNodeGroupEnum.lookupSecret, nodes); // get totp nodes from all nodes - final totpNodes = - nodes.where((node) => node.group == UiNodeGroupEnum.totp).toList(); + final totpNodes = getNodesOfGroup(UiNodeGroupEnum.totp, nodes); // get oidc nodes from all nodes - final oidcNodes = nodes.where((node) { - if (node.group == UiNodeGroupEnum.oidc) { - if (node.attributes.oneOf.isType(UiNodeInputAttributes)) { - final attributes = - node.attributes.oneOf.value as UiNodeInputAttributes; - return Platform.isAndroid - ? !attributes.value!.asString.contains('ios') - : attributes.value!.asString.contains('ios'); - } - } - return false; - }).toList(); + final oidcNodes = getNodesOfGroup(UiNodeGroupEnum.oidc, nodes); return Stack(children: [ Padding( @@ -137,6 +111,9 @@ class LoginForm extends StatelessWidget { if (defaultNodes.isNotEmpty) buildGroup(context, UiNodeGroupEnum.default_, defaultNodes, _onInputChange, _onInputSubmit), + if (codeNodes.isNotEmpty) + buildGroup(context, UiNodeGroupEnum.code, codeNodes, + _onInputChange, _onInputSubmit), if (passwordNodes.isNotEmpty) buildGroup(context, UiNodeGroupEnum.password, passwordNodes, _onInputChange, _onInputSubmit), diff --git a/flutter-ory-network/lib/pages/registration.dart b/flutter-ory-network/lib/pages/registration.dart index b0ad52c8..7447366b 100644 --- a/flutter-ory-network/lib/pages/registration.dart +++ b/flutter-ory-network/lib/pages/registration.dart @@ -1,8 +1,6 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import 'dart:io' show Platform; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ory_client/ory_client.dart'; @@ -58,47 +56,23 @@ class RegistrationFormState extends State { final nodes = state.registrationFlow!.ui.nodes; // get default nodes from all nodes - final defaultNodes = nodes.where((node) { - if (node.group == UiNodeGroupEnum.default_) { - if (node.attributes.oneOf.isType(UiNodeInputAttributes)) { - final attributes = - node.attributes.oneOf.value as UiNodeInputAttributes; - if (attributes.type == UiNodeInputAttributesTypeEnum.hidden) { - return false; - } else { - return true; - } - } - } - return false; - }).toList(); + final defaultNodes = getNodesOfGroup(UiNodeGroupEnum.default_, nodes); + + // get code nodes from all nodes + final codeNodes = getNodesOfGroup(UiNodeGroupEnum.code, nodes); // get password nodes from all nodes - final passwordNodes = - nodes.where((node) => node.group == UiNodeGroupEnum.password).toList(); + final passwordNodes = getNodesOfGroup(UiNodeGroupEnum.password, nodes); // get lookup secret nodes from all nodes - final lookupSecretNodes = nodes - .where((node) => node.group == UiNodeGroupEnum.lookupSecret) - .toList(); + final lookupSecretNodes = + getNodesOfGroup(UiNodeGroupEnum.lookupSecret, nodes); // get totp nodes from all nodes - final totpNodes = - nodes.where((node) => node.group == UiNodeGroupEnum.totp).toList(); + final totpNodes = getNodesOfGroup(UiNodeGroupEnum.totp, nodes); // get oidc nodes from all nodes - final oidcNodes = nodes.where((node) { - if (node.group == UiNodeGroupEnum.oidc) { - if (node.attributes.oneOf.isType(UiNodeInputAttributes)) { - final attributes = - node.attributes.oneOf.value as UiNodeInputAttributes; - return Platform.isAndroid - ? !attributes.value!.asString.contains('ios') - : attributes.value!.asString.contains('ios'); - } - } - return false; - }).toList(); + final oidcNodes = getNodesOfGroup(UiNodeGroupEnum.oidc, nodes); return Stack(children: [ Padding( @@ -131,6 +105,9 @@ class RegistrationFormState extends State { if (defaultNodes.isNotEmpty) buildGroup(context, UiNodeGroupEnum.default_, defaultNodes, _onInputChange, _onInputSubmit), + if (codeNodes.isNotEmpty) + buildGroup(context, UiNodeGroupEnum.code, + codeNodes, _onInputChange, _onInputSubmit), if (passwordNodes.isNotEmpty) buildGroup(context, UiNodeGroupEnum.password, passwordNodes, _onInputChange, _onInputSubmit), diff --git a/flutter-ory-network/lib/repositories/auth.dart b/flutter-ory-network/lib/repositories/auth.dart index 6923006f..ac792112 100644 --- a/flutter-ory-network/lib/repositories/auth.dart +++ b/flutter-ory-network/lib/repositories/auth.dart @@ -15,6 +15,7 @@ import 'package:one_of/one_of.dart'; import 'package:ory_client/ory_client.dart'; import 'package:collection/collection.dart'; import 'package:ory_network_flutter/services/exceptions.dart'; +import 'package:ory_network_flutter/widgets/helpers.dart'; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; import '../services/auth.dart'; @@ -89,7 +90,7 @@ class AuthRepository { } } - Future updateRegistrationFlow( + Future updateRegistrationFlow( {required String flowId, required UiNodeGroupEnum group, required String name, @@ -214,41 +215,51 @@ class AuthRepository { required String name, required String value, required List nodes}) { + // find default nodes (e.g identifier) + var inputNodes = + nodes.where((node) => node.group == UiNodeGroupEnum.default_).toList(); + // create maps from attribute names and their values + var nestedMaps = inputNodes.map((node) { + final attributes = asInputAttributes(node); + final nodeValue = getInputNodeValue(attributes); + + return generateNestedMap(attributes.name, nodeValue); + }).toList(); + // if name of submitted node is method, find all nodes that belong to the group if (name == 'method') { // get input nodes of the same group - final inputNodes = nodes.where((p0) { - if (p0.attributes.oneOf.isType(UiNodeInputAttributes)) { - final attributes = p0.attributes.oneOf.value as UiNodeInputAttributes; - // if group is password, find identifier - if (group == UiNodeGroupEnum.password && - p0.group == UiNodeGroupEnum.default_ && - attributes.name == 'identifier') { - return true; - } - return p0.group == group && + final methodNodes = nodes.where((node) { + if (isInputNode(node)) { + final attributes = asInputAttributes(node); + + return node.group == group && attributes.type != UiNodeInputAttributesTypeEnum.button && attributes.type != UiNodeInputAttributesTypeEnum.submit; } else { return false; } - }); + }).toList(); + inputNodes = inputNodes + methodNodes; // create maps from attribute names and their values - final nestedMaps = inputNodes.map((e) { - final attributes = e.attributes.oneOf.value as UiNodeInputAttributes; + final methodMaps = methodNodes.map((node) { + final attributes = asInputAttributes(node); + final nodeValue = getInputNodeValue(attributes); - return generateNestedMap( - attributes.name, attributes.value?.asString ?? ''); + return generateNestedMap(attributes.name, nodeValue); }).toList(); - - // merge nested maps into one - final mergedMap = - nestedMaps.reduce((value, element) => value.deepMerge(element)); - - return mergedMap; + // add method maps to default maps + nestedMaps.addAll(methodMaps); } else { - return {name: value}; + // add single map to default maps + nestedMaps.add({name: value}); } + + // merge nested maps into one + final mergedMap = + nestedMaps.reduce((value, element) => value.deepMerge(element)); + + return mergedMap; } RegistrationFlow changeRegistrationNodeValue( @@ -283,9 +294,8 @@ class AuthRepository { required String value}) { // get edited node final node = nodes.firstWhereOrNull((element) { - if (element.attributes.oneOf.isType(UiNodeInputAttributes)) { - return (element.attributes.oneOf.value as UiNodeInputAttributes).name == - name; + if (isInputNode(element)) { + return asInputAttributes(element).name == name; } else { return false; } diff --git a/flutter-ory-network/lib/services/auth.dart b/flutter-ory-network/lib/services/auth.dart index a5e0774e..f0724bc6 100644 --- a/flutter-ory-network/lib/services/auth.dart +++ b/flutter-ory-network/lib/services/auth.dart @@ -42,7 +42,7 @@ class AuthService { } } - /// Create login flow + /// Create login flow with [aal] Future createLoginFlow({required String aal}) async { try { final token = await storage.getToken(); @@ -114,6 +114,14 @@ class AuthService { ..provider = value['provider'] ..idToken = value['id_token'] ..idTokenNonce = value['nonce'])); + case UiNodeGroupEnum.code: + oneOf = OneOf.fromValue1( + value: UpdateLoginFlowWithCodeMethod((b) => b + ..csrfToken = '' + ..method = group.name + ..identifier = value['identifier'] + ..code = value['resend'] != null ? null : value['code'] + ..resend = value['resend'])); // if method is not implemented, throw exception default: @@ -242,6 +250,14 @@ class AuthService { ..provider = value['provider'] ..idToken = value['id_token'] ..idTokenNonce = value['nonce'])); + case UiNodeGroupEnum.code: + oneOf = OneOf.fromValue1( + value: UpdateRegistrationFlowWithCodeMethod((b) => b + ..csrfToken = '' + ..method = group.name + ..traits = JsonObject(value['traits']) + ..code = value['resend'] != null ? null : value['code'] + ..resend = value['resend'])); // if method is not implemented, throw exception default: diff --git a/flutter-ory-network/lib/widgets/helpers.dart b/flutter-ory-network/lib/widgets/helpers.dart index ad6f03ca..9844ceec 100644 --- a/flutter-ory-network/lib/widgets/helpers.dart +++ b/flutter-ory-network/lib/widgets/helpers.dart @@ -1,7 +1,10 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 +import 'dart:io'; + import 'package:bloc/bloc.dart'; +import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:ory_client/ory_client.dart'; @@ -9,6 +12,7 @@ import 'nodes/input.dart'; import 'nodes/input_submit.dart'; import 'nodes/text.dart'; +/// Returns color of a message depending on its [type] getMessageColor(UiTextTypeEnum type) { switch (type) { case UiTextTypeEnum.success: @@ -20,6 +24,7 @@ getMessageColor(UiTextTypeEnum type) { } } +/// Returns error widget with corresponding [message] buildFlowNotCreated(BuildContext context, String? message) { if (message != null) { return Center( @@ -32,6 +37,9 @@ buildFlowNotCreated(BuildContext context, String? message) { } } +/// Returns a form for a specific [group] containing multiple [nodes]. +/// Node changes and submits are handled by +/// [onInputChange] and [onInputSubmit], respectively buildGroup( BuildContext context, UiNodeGroupEnum group, @@ -65,6 +73,9 @@ buildGroup( ); } +/// Returns input node for [node] from a form associated with [formKey]. +/// Node changes and submits are handled by +/// [onInputChange] and [onInputSubmit], respectively. buildInputNode( BuildContext context, GlobalKey formKey, @@ -72,8 +83,8 @@ buildInputNode( void Function(BuildContext, String, String) onInputChange, void Function(BuildContext, UiNodeGroupEnum, String, String) onInputSubmit) { - final inputNode = node.attributes.oneOf.value as UiNodeInputAttributes; - switch (inputNode.type) { + final attributes = asInputAttributes(node); + switch (attributes.type) { case UiNodeInputAttributesTypeEnum.submit: return InputSubmitNode( node: node, @@ -90,3 +101,51 @@ buildInputNode( return InputNode(node: node, onChange: onInputChange); } } + +/// Returns nodes with a type of [group] from all available [nodes] +List getNodesOfGroup(UiNodeGroupEnum group, BuiltList nodes) { + return nodes.where((node) { + if (node.group == group) { + if (group == UiNodeGroupEnum.oidc) { + if (isInputNode(node)) { + final attributes = asInputAttributes(node); + return Platform.isAndroid + ? !getInputNodeValue(attributes).contains('ios') + : getInputNodeValue(attributes).contains('ios'); + } + } else { + if (isInputNode(node)) { + final attributes = asInputAttributes(node); + if (attributes.type == UiNodeInputAttributesTypeEnum.hidden) { + return false; + } else { + return true; + } + } + } + } + return false; + }).toList(); +} + +/// Returns true if [node] is of type UiNodeInputAttributes. +/// Otherwise, returns false +bool isInputNode(UiNode node) { + return node.attributes.oneOf.isType(UiNodeInputAttributes); +} + +/// Returns string value of [attributes] +String getInputNodeValue(UiNodeInputAttributes attributes) { + return attributes.value?.asString ?? ''; +} + +/// Returns input attributes of a [node]. +/// Attributes must be of type UiNodeInputAttributes +UiNodeInputAttributes asInputAttributes(UiNode node) { + if (isInputNode(node)) { + return node.attributes.oneOf.value as UiNodeInputAttributes; + } else { + throw ArgumentError( + 'attributes of this node are not of type UiNodeInputAttributes'); + } +} diff --git a/flutter-ory-network/lib/widgets/nodes/input.dart b/flutter-ory-network/lib/widgets/nodes/input.dart index 05731b3b..e5b8de86 100644 --- a/flutter-ory-network/lib/widgets/nodes/input.dart +++ b/flutter-ory-network/lib/widgets/nodes/input.dart @@ -26,8 +26,7 @@ class _InputNodeState extends State { @override void initState() { super.initState(); - final attributes = - widget.node.attributes.oneOf.value as UiNodeInputAttributes; + final attributes = asInputAttributes(widget.node); // if this is a password field, hide the text if (attributes.name == 'password') { setState(() { @@ -65,8 +64,7 @@ class _InputNodeState extends State { @override Widget build(BuildContext context) { - final attributes = - widget.node.attributes.oneOf.value as UiNodeInputAttributes; + final attributes = asInputAttributes(widget.node); return BlocListener( bloc: (context).read(), @@ -75,20 +73,16 @@ class _InputNodeState extends State { // find current node in updated state if (state is LoginState) { node = state.loginFlow?.ui.nodes.firstWhereOrNull((element) { - if (element.attributes.oneOf.isType(UiNodeInputAttributes)) { - return (element.attributes.oneOf.value as UiNodeInputAttributes) - .name == - attributes.name; + if (isInputNode(element)) { + return asInputAttributes(element).name == attributes.name; } else { return false; } }); } else if (state is RegistrationState) { node = state.registrationFlow?.ui.nodes.firstWhereOrNull((element) { - if (element.attributes.oneOf.isType(UiNodeInputAttributes)) { - return (element.attributes.oneOf.value as UiNodeInputAttributes) - .name == - attributes.name; + if (isInputNode(element)) { + return asInputAttributes(element).name == attributes.name; } else { return false; } @@ -96,11 +90,10 @@ class _InputNodeState extends State { } // assign new value of node to text controller - textEditingController.text = - (node?.attributes.oneOf.value as UiNodeInputAttributes) - .value - ?.asString ?? - ''; + if (node != null && isInputNode(node)) { + final attributes = asInputAttributes(node); + textEditingController.text = getInputNodeValue(attributes); + } }, child: Padding( padding: const EdgeInsets.only(bottom: 20.0), diff --git a/flutter-ory-network/lib/widgets/nodes/input_submit.dart b/flutter-ory-network/lib/widgets/nodes/input_submit.dart index e1c4884f..1c36e2ae 100644 --- a/flutter-ory-network/lib/widgets/nodes/input_submit.dart +++ b/flutter-ory-network/lib/widgets/nodes/input_submit.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:ory_client/ory_client.dart'; +import 'package:ory_network_flutter/widgets/helpers.dart'; class InputSubmitNode extends StatelessWidget { final GlobalKey formKey; @@ -19,9 +20,8 @@ class InputSubmitNode extends StatelessWidget { @override Widget build(BuildContext context) { - final value = node.attributes.oneOf.isType(UiNodeInputAttributes) - ? (node.attributes.oneOf.value as UiNodeInputAttributes).value?.asString - : null; + final attributes = asInputAttributes(node); + final value = getInputNodeValue(attributes); final provider = _getProviderName(value); return SizedBox( width: double.infinity, @@ -40,21 +40,22 @@ class InputSubmitNode extends StatelessWidget { ); } - _getProviderName(String? value) { - if (value == null) { - return ''; - } else if (value.contains('google')) { + _getProviderName(String value) { + if (value.contains('google')) { return 'google'; } else if (value.contains('apple')) { return 'apple'; } else { - return ''; + return value; } } onPressed(BuildContext context) { - if (formKey.currentState!.validate()) { - final attributes = node.attributes.oneOf.value as UiNodeInputAttributes; + final attributes = asInputAttributes(node); + + // if attribute is method, validate the form + if ((attributes.name == 'method' && formKey.currentState!.validate()) || + attributes.name != 'method') { final type = attributes.type; // if attribute type is a button with value 'false', set its value to true on submit if (type == UiNodeInputAttributesTypeEnum.button || diff --git a/flutter-ory-network/pubspec.lock b/flutter-ory-network/pubspec.lock index 1535585a..d0dec943 100644 --- a/flutter-ory-network/pubspec.lock +++ b/flutter-ory-network/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: "direct main" description: name: built_value - sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + sha256: "723b4021e903217dfc445ec4cf5b42e27975aece1fc4ebbc1ca6329c2d9fb54e" url: "https://pub.dev" source: hosted - version: "8.6.3" + version: "8.7.0" characters: dependency: transitive description: @@ -428,10 +428,10 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: "939e9172a378ec4eaeb7f71eeddac9b55ebd0e8546d336daec476a68e5279766" + sha256: "6b08be471f82ff84058d528e1cb01f4f53084fa648b751310cdd1ac39b612d8e" url: "https://pub.dev" source: hosted - version: "0.12.0+5" + version: "0.12.1" graphs: dependency: transitive description: @@ -572,10 +572,10 @@ packages: dependency: "direct main" description: name: ory_client - sha256: "2eb2fb2ad8abe8093c970420972e5cb4a932640d031c52cae962dc444e0a7a54" + sha256: c8d672ee192c77aa4231afbbf7fa6d23387e2a09ac9cf14a596dfcf5b0ecc29b url: "https://pub.dev" source: hosted - version: "1.2.11" + version: "1.2.16" package_config: dependency: transitive description: