mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
Support msisdn registration
This commit is contained in:
parent
f63a0c490a
commit
bdb7ef1ae2
4 changed files with 456 additions and 112 deletions
|
@ -294,7 +294,7 @@
|
|||
AuthInputsView *authInputsview = (AuthInputsView*)self.authInputsView;
|
||||
|
||||
// Show the 3rd party ids screen if it is not shown yet
|
||||
if (authInputsview.isThirdPartyIdentifiersSupported && authInputsview.isThirdPartyIdentifiersHidden)
|
||||
if (authInputsview.areThirdPartyIdentifiersSupported && authInputsview.isThirdPartyIdentifiersHidden)
|
||||
{
|
||||
[self dismissKeyboard];
|
||||
|
||||
|
@ -320,6 +320,9 @@
|
|||
[self.authenticationActivityIndicator stopAnimating];
|
||||
|
||||
// Show the supported 3rd party ids which may be added to the account
|
||||
// Retrieve the MCC from the SIM card information (Note: the phone book country code is not defined yet)
|
||||
authInputsview.isoCountryCode = [MXKAppSettings standardAppSettings].phonebookCountryCode;
|
||||
authInputsview.delegate = self;
|
||||
authInputsview.thirdPartyIdentifiersHidden = NO;
|
||||
|
||||
[self updateRegistrationScreenWithThirdPartyIdentifiersHidden:NO];
|
||||
|
@ -419,6 +422,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)onSuccessfulLogin:(MXCredentials*)credentials
|
||||
{
|
||||
// Check whether a third party identifiers has not been used
|
||||
if ([self.authInputsView isKindOfClass:AuthInputsView.class])
|
||||
{
|
||||
AuthInputsView *authInputsview = (AuthInputsView*)self.authInputsView;
|
||||
if (authInputsview.isThirdPartyIdentifierPending)
|
||||
{
|
||||
// Alert user
|
||||
if (alert)
|
||||
{
|
||||
[alert dismiss:NO];
|
||||
}
|
||||
|
||||
alert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"warning", @"Vector", nil) message:NSLocalizedStringFromTable(@"auth_add_email_and_phone_warning", @"Vector", nil) style:MXKAlertStyleAlert];
|
||||
|
||||
[alert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleCancel handler:^(MXKAlert *alert)
|
||||
{
|
||||
[super onSuccessfulLogin:credentials];
|
||||
}];
|
||||
|
||||
[alert showInViewController:self];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[super onSuccessfulLogin:credentials];
|
||||
}
|
||||
|
||||
- (void)updateForgotPwdButtonVisibility
|
||||
{
|
||||
self.forgotPasswordButton.hidden = (self.authType != MXKAuthenticationTypeLogin);
|
||||
|
@ -598,4 +630,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
#pragma mark - MXKAuthInputsViewDelegate
|
||||
|
||||
- (void)authInputsView:(MXKAuthInputsView *)authInputsView presentViewController:(UIViewController*)viewControllerToPresent
|
||||
{
|
||||
[self dismissKeyboard];
|
||||
|
||||
[self presentViewController:viewControllerToPresent animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)authInputsViewDidCancelOperation:(MXKAuthInputsView *)authInputsView
|
||||
{
|
||||
[self cancel];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -53,7 +53,17 @@
|
|||
/**
|
||||
Tell whether some third-party identifiers may be added during the account registration.
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL isThirdPartyIdentifiersSupported;
|
||||
@property (nonatomic, readonly) BOOL areThirdPartyIdentifiersSupported;
|
||||
|
||||
/**
|
||||
Tell whether at least one third-party identifier is required to create a new account.
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL isThirdPartyIdentifierRequired;
|
||||
|
||||
/**
|
||||
Tell whether all the supported third-party identifiers are required to create a new account.
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL areAllThirdPartyIdentifiersRequired;
|
||||
|
||||
/**
|
||||
Update the registration inputs layout by hidding the third-party identifiers fields (YES by default).
|
||||
|
@ -61,6 +71,11 @@
|
|||
*/
|
||||
@property (nonatomic, getter=isThirdPartyIdentifiersHidden) BOOL thirdPartyIdentifiersHidden;
|
||||
|
||||
/**
|
||||
Tell whether a second third-party identifier is waiting for being added to the new account.
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL isThirdPartyIdentifierPending;
|
||||
|
||||
/**
|
||||
The current selected country code
|
||||
*/
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
The current msisdn validation
|
||||
*/
|
||||
MXK3PID *submittedMSISDN;
|
||||
UINavigationController *phoneNumberPickerNavigationController;
|
||||
CountryPickerViewController *phoneNumberCountryPicker;
|
||||
NBPhoneNumber *newPhoneNumber;
|
||||
|
||||
|
@ -43,6 +44,11 @@
|
|||
NSDictionary *externalRegistrationParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
The current view container displayed at last position.
|
||||
*/
|
||||
@property (nonatomic) UIView *currentLastContainer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AuthInputsView
|
||||
|
@ -72,6 +78,7 @@
|
|||
self.messageLabel.numberOfLines = 0;
|
||||
|
||||
_thirdPartyIdentifiersHidden = YES;
|
||||
_isThirdPartyIdentifierPending = NO;
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
|
@ -86,18 +93,10 @@
|
|||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
CGRect lastItemFrame;
|
||||
|
||||
if (self.recaptchaWebView.isHidden)
|
||||
if (_currentLastContainer)
|
||||
{
|
||||
lastItemFrame = self.repeatPasswordContainer.frame;
|
||||
self.currentLastContainer = _currentLastContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastItemFrame = self.recaptchaWebView.frame;
|
||||
}
|
||||
|
||||
self.viewHeightConstraint.constant = lastItemFrame.origin.y + lastItemFrame.size.height;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
@ -140,8 +139,7 @@
|
|||
self.userLoginContainer.hidden = NO;
|
||||
self.passwordContainer.hidden = NO;
|
||||
|
||||
CGRect frame = self.passwordContainer.frame;
|
||||
self.viewHeightConstraint.constant = frame.origin.y + frame.size.height;
|
||||
self.currentLastContainer = self.passwordContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -226,35 +224,54 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
if (self.isEmailIdentityFlowRequired && !self.emailTextField.text.length)
|
||||
// Check email field
|
||||
if (self.isEmailIdentityFlowSupported && !self.emailTextField.text.length)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Missing email");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_missing_email", @"Vector", nil);
|
||||
}
|
||||
else if (self.isMSISDNFlowRequired && !self.phoneTextField.text.length)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Missing phone");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_missing_phone", @"Vector", nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (self.emailTextField.text.length)
|
||||
if (self.areAllThirdPartyIdentifiersRequired)
|
||||
{
|
||||
// Check validity of the non empty email
|
||||
if (![MXTools isEmailAddress:self.emailTextField.text])
|
||||
NSLog(@"[AuthInputsView] Missing email");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_missing_email", @"Vector", nil);
|
||||
}
|
||||
else if (self.isMSISDNFlowSupported && !self.phoneTextField.text.length && self.isThirdPartyIdentifierRequired)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Missing email or phone number");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_missing_email_or_phone", @"Vector", nil);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorMsg)
|
||||
{
|
||||
// Check phone field
|
||||
if (self.isMSISDNFlowSupported && !self.phoneTextField.text.length)
|
||||
{
|
||||
if (self.areAllThirdPartyIdentifiersRequired)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Invalid email");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_invalid_email", @"Vector", nil);
|
||||
NSLog(@"[AuthInputsView] Missing phone");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_missing_phone", @"Vector", nil);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorMsg && newPhoneNumber)
|
||||
if (!errorMsg)
|
||||
{
|
||||
// Check validity of the non empty phone
|
||||
if (![[NBPhoneNumberUtil sharedInstance] isValidNumber:newPhoneNumber])
|
||||
// Check email/phone validity
|
||||
if (self.emailTextField.text.length)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Invalid phone number");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_invalid_phone", @"Vector", nil);
|
||||
// Check validity of the non empty email
|
||||
if (![MXTools isEmailAddress:self.emailTextField.text])
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Invalid email");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_invalid_email", @"Vector", nil);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorMsg && newPhoneNumber)
|
||||
{
|
||||
// Check validity of the non empty phone
|
||||
if (![[NBPhoneNumberUtil sharedInstance] isValidNumber:newPhoneNumber])
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Invalid phone number");
|
||||
errorMsg = NSLocalizedStringFromTable(@"auth_invalid_phone", @"Vector", nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -347,9 +364,11 @@
|
|||
}
|
||||
else if (type == MXKAuthenticationTypeRegister)
|
||||
{
|
||||
// Check whether an email has been set, and if it is not handled yet
|
||||
if (!self.emailContainer.isHidden && self.emailTextField.text.length && !self.isEmailIdentityFlowCompleted)
|
||||
// Check whether a phone number has been set, and if it is not handled yet
|
||||
if (newPhoneNumber && !self.isMSISDNFlowCompleted)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Prepare msisdn stage");
|
||||
|
||||
// Retrieve the REST client from delegate
|
||||
MXRestClient *restClient;
|
||||
|
||||
|
@ -360,6 +379,65 @@
|
|||
|
||||
if (restClient)
|
||||
{
|
||||
// Check whether a second 3pid is available
|
||||
_isThirdPartyIdentifierPending = (!self.emailContainer.isHidden && self.emailTextField.text.length && !self.isEmailIdentityFlowCompleted);
|
||||
|
||||
// Launch msisdn validation
|
||||
NSString *e164 = [[NBPhoneNumberUtil sharedInstance] format:newPhoneNumber numberFormat:NBEPhoneNumberFormatE164 error:nil];
|
||||
NSString *msisdn;
|
||||
if ([e164 hasPrefix:@"+"])
|
||||
{
|
||||
msisdn = [e164 substringFromIndex:1];
|
||||
}
|
||||
else if ([e164 hasPrefix:@"00"])
|
||||
{
|
||||
msisdn = [e164 substringFromIndex:2];
|
||||
}
|
||||
submittedMSISDN = [[MXK3PID alloc] initWithMedium:kMX3PIDMediumMSISDN andAddress:msisdn];
|
||||
|
||||
[submittedMSISDN requestValidationTokenWithMatrixRestClient:restClient
|
||||
nextLink:nil
|
||||
success:^{
|
||||
|
||||
[self showValidationMSISDNDialogToPrepareParameters:callback];
|
||||
|
||||
} failure:^(NSError *error) {
|
||||
|
||||
NSLog(@"[AuthInputsView] Failed to request msisdn token");
|
||||
|
||||
// Ignore connection cancellation error
|
||||
if (([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
callback(nil);
|
||||
|
||||
}];
|
||||
|
||||
// Async response
|
||||
return;
|
||||
}
|
||||
NSLog(@"[AuthInputsView] Authentication failed during the msisdn stage");
|
||||
}
|
||||
// Check whether an email has been set, and if it is not handled yet
|
||||
else if (!self.emailContainer.isHidden && self.emailTextField.text.length && !self.isEmailIdentityFlowCompleted)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Prepare email identity stage");
|
||||
|
||||
// Retrieve the REST client from delegate
|
||||
MXRestClient *restClient;
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(authInputsViewEmailValidationRestClient:)])
|
||||
{
|
||||
restClient = [self.delegate authInputsViewEmailValidationRestClient:self];
|
||||
}
|
||||
|
||||
if (restClient)
|
||||
{
|
||||
// Check whether a second 3pid is available
|
||||
_isThirdPartyIdentifierPending = (newPhoneNumber && !self.isMSISDNFlowCompleted);
|
||||
|
||||
// Launch email validation
|
||||
submittedEmail = [[MXK3PID alloc] initWithMedium:kMX3PIDMediumEmail andAddress:self.emailTextField.text];
|
||||
|
||||
|
@ -381,6 +459,7 @@
|
|||
@"auth": @{@"session":currentSession.session, @"threepid_creds": @{@"client_secret": submittedEmail.clientSecret, @"id_server": identServerURL.host, @"sid": submittedEmail.sid}, @"type": kMXLoginFlowTypeEmailIdentity},
|
||||
@"username": self.userLoginTextField.text,
|
||||
@"password": self.passWordTextField.text,
|
||||
@"bind_msisdn": [NSNumber numberWithBool:self.isMSISDNFlowCompleted],
|
||||
@"bind_email": @(YES)
|
||||
};
|
||||
|
||||
|
@ -408,13 +487,12 @@
|
|||
// Async response
|
||||
return;
|
||||
}
|
||||
else if (self.isEmailIdentityFlowRequired)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Authentication failed during the email identity stage");
|
||||
}
|
||||
NSLog(@"[AuthInputsView] Authentication failed during the email identity stage");
|
||||
}
|
||||
else if (self.isRecaptchaFlowRequired)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Prepare reCaptcha stage");
|
||||
|
||||
[self displayRecaptchaForm:^(NSString *response) {
|
||||
|
||||
if (response.length)
|
||||
|
@ -423,6 +501,7 @@
|
|||
@"auth": @{@"session":currentSession.session, @"response": response, @"type": kMXLoginFlowTypeRecaptcha},
|
||||
@"username": self.userLoginTextField.text,
|
||||
@"password": self.passWordTextField.text,
|
||||
@"bind_msisdn": [NSNumber numberWithBool:self.isMSISDNFlowCompleted],
|
||||
@"bind_email": [NSNumber numberWithBool:self.isEmailIdentityFlowCompleted]
|
||||
};
|
||||
|
||||
|
@ -445,6 +524,7 @@
|
|||
@"auth": @{@"session":currentSession.session, @"type": kMXLoginFlowTypeDummy},
|
||||
@"username": self.userLoginTextField.text,
|
||||
@"password": self.passWordTextField.text,
|
||||
@"bind_msisdn": @(NO),
|
||||
@"bind_email": @(NO)
|
||||
};
|
||||
}
|
||||
|
@ -470,9 +550,20 @@
|
|||
{
|
||||
currentSession.completed = completedStages;
|
||||
|
||||
// Check the supported use case
|
||||
if ([completedStages indexOfObject:kMXLoginFlowTypeEmailIdentity] != NSNotFound && self.isRecaptchaFlowRequired)
|
||||
// Check the supported use cases
|
||||
if ([completedStages indexOfObject:kMXLoginFlowTypeMSISDN] != NSNotFound && self.isThirdPartyIdentifierPending)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Prepare a new third-party stage");
|
||||
|
||||
// Here an email address is available, we add it to the authentication session.
|
||||
[self prepareParameters:callback];
|
||||
|
||||
return;
|
||||
}
|
||||
else if ([completedStages indexOfObject:kMXLoginFlowTypeEmailIdentity] != NSNotFound && self.isRecaptchaFlowRequired)
|
||||
{
|
||||
NSLog(@"[AuthInputsView] Display reCaptcha stage");
|
||||
|
||||
[self displayRecaptchaForm:^(NSString *response) {
|
||||
|
||||
if (response.length)
|
||||
|
@ -493,6 +584,7 @@
|
|||
@"auth": @{@"session": currentSession.session, @"response": response, @"type": kMXLoginFlowTypeRecaptcha},
|
||||
@"username": self.userLoginTextField.text,
|
||||
@"password": self.passWordTextField.text,
|
||||
@"bind_msisdn": [NSNumber numberWithBool:self.isMSISDNFlowCompleted],
|
||||
@"bind_email": @(YES)
|
||||
};
|
||||
}
|
||||
|
@ -622,11 +714,22 @@
|
|||
[self.userLoginTextField resignFirstResponder];
|
||||
[self.passWordTextField resignFirstResponder];
|
||||
[self.emailTextField resignFirstResponder];
|
||||
[self.phoneTextField resignFirstResponder];
|
||||
[self.repeatPasswordTextField resignFirstResponder];
|
||||
|
||||
[super dismissKeyboard];
|
||||
}
|
||||
|
||||
- (void)dismissCountryPicker
|
||||
{
|
||||
[phoneNumberCountryPicker withdrawViewControllerAnimated:YES completion:nil];
|
||||
[phoneNumberCountryPicker destroy];
|
||||
phoneNumberCountryPicker = nil;
|
||||
|
||||
[phoneNumberPickerNavigationController dismissViewControllerAnimated:YES completion:nil];
|
||||
phoneNumberPickerNavigationController = nil;
|
||||
}
|
||||
|
||||
- (NSString*)userId
|
||||
{
|
||||
return self.userLoginTextField.text;
|
||||
|
@ -637,19 +740,98 @@
|
|||
return self.passWordTextField.text;
|
||||
}
|
||||
|
||||
- (void)setCurrentLastContainer:(UIView*)currentLastContainer
|
||||
{
|
||||
_currentLastContainer = currentLastContainer;
|
||||
|
||||
CGRect frame = _currentLastContainer.frame;
|
||||
self.viewHeightConstraint.constant = frame.origin.y + frame.size.height;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (BOOL)isThirdPartyIdentifiersSupported
|
||||
- (BOOL)areThirdPartyIdentifiersSupported
|
||||
{
|
||||
return (self.isEmailIdentityFlowSupported || self.isMSISDNFlowSupported);
|
||||
}
|
||||
|
||||
- (BOOL)isThirdPartyIdentifierRequired
|
||||
{
|
||||
// Check first whether some 3pids are supported
|
||||
if (!self.areThirdPartyIdentifiersSupported)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Check whether an account may be created without third-party identifiers.
|
||||
for (MXLoginFlow *loginFlow in currentSession.flows)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeDummy] != NSNotFound || [loginFlow.type isEqualToString:kMXLoginFlowTypeDummy])
|
||||
{
|
||||
// The dummy flow is supported, the 3pid are then optional.
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ((loginFlow.stages.count == 1 && [loginFlow.stages[0] isEqualToString:kMXLoginFlowTypeRecaptcha]) || [loginFlow.type isEqualToString:kMXLoginFlowTypeRecaptcha])
|
||||
{
|
||||
// The recaptcha flow is supported alone, the 3pids are then optional.
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ((loginFlow.stages.count == 1 && [loginFlow.stages[0] isEqualToString:kMXLoginFlowTypePassword]) || [loginFlow.type isEqualToString:kMXLoginFlowTypePassword])
|
||||
{
|
||||
// The password flow is supported alone, the 3pids are then optional.
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)areAllThirdPartyIdentifiersRequired
|
||||
{
|
||||
// Check first whether some 3pids are required
|
||||
if (!self.isThirdPartyIdentifierRequired)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL isEmailIdentityFlowSupported = self.isEmailIdentityFlowSupported;
|
||||
BOOL isMSISDNFlowSupported = self.isMSISDNFlowSupported;
|
||||
|
||||
for (MXLoginFlow *loginFlow in currentSession.flows)
|
||||
{
|
||||
if (isEmailIdentityFlowSupported)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeEmailIdentity] == NSNotFound)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
else if (isMSISDNFlowSupported)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeMSISDN] == NSNotFound)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (isMSISDNFlowSupported)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeMSISDN] == NSNotFound)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setThirdPartyIdentifiersHidden:(BOOL)thirdPartyIdentifiersHidden
|
||||
{
|
||||
[self hideInputsContainer];
|
||||
|
||||
CGFloat viewHeight = 0;
|
||||
UIView *lastViewContainer;
|
||||
|
||||
if (thirdPartyIdentifiersHidden)
|
||||
{
|
||||
|
@ -661,13 +843,13 @@
|
|||
self.passwordContainer.hidden = NO;
|
||||
self.repeatPasswordContainer.hidden = NO;
|
||||
|
||||
viewHeight = 150;
|
||||
lastViewContainer = self.repeatPasswordContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (self.isEmailIdentityFlowSupported)
|
||||
{
|
||||
if (self.isEmailIdentityFlowRequired)
|
||||
if (self.isThirdPartyIdentifierRequired)
|
||||
{
|
||||
self.emailTextField.placeholder = NSLocalizedStringFromTable(@"auth_email_placeholder", @"Vector", nil);
|
||||
}
|
||||
|
@ -681,12 +863,12 @@
|
|||
self.messageLabel.hidden = NO;
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_add_email_message", @"Vector", nil);
|
||||
|
||||
viewHeight = 50;
|
||||
lastViewContainer = self.emailContainer;
|
||||
}
|
||||
|
||||
if (self.isMSISDNFlowSupported)
|
||||
{
|
||||
if (self.isMSISDNFlowRequired)
|
||||
if (self.isThirdPartyIdentifierRequired)
|
||||
{
|
||||
self.phoneTextField.placeholder = NSLocalizedStringFromTable(@"auth_phone_placeholder", @"Vector", nil);
|
||||
}
|
||||
|
@ -703,17 +885,22 @@
|
|||
|
||||
self.phoneContainerTopConstraint.constant = 50;
|
||||
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_add_email_msisdn_message", @"Vector", nil);
|
||||
|
||||
viewHeight = 100;
|
||||
if (self.areAllThirdPartyIdentifiersRequired)
|
||||
{
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_add_email_and_phone_message", @"Vector", nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_add_email_phone_message", @"Vector", nil);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.messageLabel.hidden = NO;
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_add_phone_message", @"Vector", nil);
|
||||
|
||||
viewHeight = 50;
|
||||
}
|
||||
|
||||
lastViewContainer = self.phoneContainer;
|
||||
}
|
||||
|
||||
if (!self.messageLabel.isHidden)
|
||||
|
@ -722,15 +909,14 @@
|
|||
|
||||
CGRect frame = self.messageLabel.frame;
|
||||
|
||||
CGFloat offset = frame.origin.x + frame.size.height;
|
||||
CGFloat offset = frame.origin.y + frame.size.height;
|
||||
|
||||
self.emailContainerTopConstraint.constant = offset;
|
||||
self.phoneContainerTopConstraint.constant += offset;
|
||||
viewHeight += offset;
|
||||
}
|
||||
}
|
||||
|
||||
self.viewHeightConstraint.constant = viewHeight;
|
||||
self.currentLastContainer = lastViewContainer;
|
||||
|
||||
_thirdPartyIdentifiersHidden = thirdPartyIdentifiersHidden;
|
||||
}
|
||||
|
@ -743,7 +929,13 @@
|
|||
phoneNumberCountryPicker.delegate = self;
|
||||
phoneNumberCountryPicker.showCountryCallingCode = YES;
|
||||
|
||||
[self.delegate authInputsView:self presentViewController:phoneNumberCountryPicker];
|
||||
phoneNumberPickerNavigationController = [[UINavigationController alloc] init];
|
||||
[phoneNumberPickerNavigationController pushViewController:phoneNumberCountryPicker animated:NO];
|
||||
|
||||
UIBarButtonItem *leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"back_icon"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissCountryPicker)];
|
||||
phoneNumberCountryPicker.navigationItem.leftBarButtonItem = leftBarButtonItem;
|
||||
|
||||
[self.delegate authInputsView:self presentViewController:phoneNumberPickerNavigationController];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -780,8 +972,7 @@
|
|||
newPhoneNumber = [[NBPhoneNumberUtil sharedInstance] parse:self.phoneTextField.text defaultRegion:isoCountryCode error:nil];
|
||||
[self formatNewPhoneNumber];
|
||||
|
||||
[countryPickerViewController withdrawViewControllerAnimated:YES completion:nil];
|
||||
phoneNumberCountryPicker = nil;
|
||||
[self dismissCountryPicker];
|
||||
}
|
||||
|
||||
#pragma mark - UITextField delegate
|
||||
|
@ -844,6 +1035,8 @@
|
|||
// Hide other items
|
||||
self.messageLabel.hidden = YES;
|
||||
self.recaptchaWebView.hidden = YES;
|
||||
|
||||
_currentLastContainer = nil;
|
||||
}
|
||||
|
||||
- (void)formatNewPhoneNumber
|
||||
|
@ -889,8 +1082,7 @@
|
|||
self.messageLabel.text = NSLocalizedStringFromTable(@"auth_recaptcha_message", @"Vector", nil);
|
||||
|
||||
self.recaptchaWebView.hidden = NO;
|
||||
CGRect frame = self.recaptchaWebView.frame;
|
||||
self.viewHeightConstraint.constant = frame.origin.y + frame.size.height;
|
||||
self.currentLastContainer = self.recaptchaWebView;
|
||||
|
||||
[self.recaptchaWebView openRecaptchaWidgetWithSiteKey:siteKey fromHomeServer:restClient.homeserver callback:callback];
|
||||
|
||||
|
@ -1013,6 +1205,133 @@
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (void)showValidationMSISDNDialogToPrepareParameters:(void (^)(NSDictionary *parameters))callback
|
||||
{
|
||||
__weak typeof(self) weakSelf = self;
|
||||
|
||||
if (inputsAlert)
|
||||
{
|
||||
[inputsAlert dismiss:NO];
|
||||
}
|
||||
|
||||
inputsAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"auth_msisdn_validation_title", @"Vector", nil)
|
||||
message:NSLocalizedStringFromTable(@"auth_msisdn_validation_message", @"Vector", nil)
|
||||
style:MXKAlertStyleAlert];
|
||||
inputsAlert.cancelButtonIndex = [inputsAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"abort"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert){
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->inputsAlert = nil;
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(authInputsViewDidCancelOperation:)])
|
||||
{
|
||||
[self.delegate authInputsViewDidCancelOperation:self];
|
||||
}
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
[inputsAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
|
||||
textField.secureTextEntry = NO;
|
||||
textField.placeholder = nil;
|
||||
textField.keyboardType = UIKeyboardTypeDecimalPad;
|
||||
|
||||
}];
|
||||
|
||||
[inputsAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"submit"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) {
|
||||
|
||||
UITextField *textField = [alert textFieldAtIndex:0];
|
||||
NSString *smsCode = textField.text;
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->inputsAlert = nil;
|
||||
|
||||
if (smsCode.length)
|
||||
{
|
||||
[self->submittedMSISDN submitValidationToken:smsCode success:^{
|
||||
|
||||
// Retrieve the REST client from delegate
|
||||
MXRestClient *restClient;
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(authInputsViewEmailValidationRestClient:)])
|
||||
{
|
||||
restClient = [self.delegate authInputsViewEmailValidationRestClient:self];
|
||||
}
|
||||
|
||||
NSURL *identServerURL = [NSURL URLWithString:restClient.identityServer];
|
||||
NSDictionary *parameters;
|
||||
parameters = @{
|
||||
@"auth": @{@"session":self->currentSession.session, @"threepid_creds": @{@"client_secret": self->submittedMSISDN.clientSecret, @"id_server": identServerURL.host, @"sid": self->submittedMSISDN.sid}, @"type": kMXLoginFlowTypeMSISDN},
|
||||
@"username": self.userLoginTextField.text,
|
||||
@"password": self.passWordTextField.text,
|
||||
@"bind_msisdn": @(YES),
|
||||
@"bind_email": [NSNumber numberWithBool:self.isEmailIdentityFlowCompleted]
|
||||
};
|
||||
|
||||
callback(parameters);
|
||||
|
||||
} failure:^(NSError *error) {
|
||||
|
||||
NSLog(@"[AuthInputsView] Failed to submit the sms token");
|
||||
|
||||
// Ignore connection cancellation error
|
||||
if (([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Alert user
|
||||
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
|
||||
NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
|
||||
if (!title)
|
||||
{
|
||||
if (msg)
|
||||
{
|
||||
title = msg;
|
||||
msg = nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = [NSBundle mxk_localizedStringForKey:@"error"];
|
||||
}
|
||||
}
|
||||
|
||||
self->inputsAlert = [[MXKAlert alloc] initWithTitle:title message:msg style:MXKAlertStyleAlert];
|
||||
self->inputsAlert.cancelButtonIndex = [self->inputsAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) {
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->inputsAlert = nil;
|
||||
|
||||
// Ask again for the token
|
||||
[self showValidationMSISDNDialogToPrepareParameters:callback];
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
self->inputsAlert.mxkAccessibilityIdentifier = @"AuthInputsViewErrorAlert";
|
||||
[self.delegate authInputsView:self presentMXKAlert:self->inputsAlert];
|
||||
|
||||
}];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ask again for the token
|
||||
[self showValidationMSISDNDialogToPrepareParameters:callback];
|
||||
}
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
inputsAlert.mxkAccessibilityIdentifier = @"AuthInputsViewMsisdnValidationAlert";
|
||||
[self.delegate authInputsView:self presentMXKAlert:inputsAlert];
|
||||
}
|
||||
|
||||
- (BOOL)isPasswordBasedFlowSupported
|
||||
{
|
||||
if (currentSession)
|
||||
|
@ -1045,24 +1364,6 @@
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isEmailIdentityFlowRequired
|
||||
{
|
||||
if (currentSession && currentSession.flows)
|
||||
{
|
||||
for (MXLoginFlow *loginFlow in currentSession.flows)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeEmailIdentity] == NSNotFound && ![loginFlow.type isEqualToString:kMXLoginFlowTypeEmailIdentity])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isEmailIdentityFlowCompleted
|
||||
{
|
||||
if (currentSession && currentSession.completed)
|
||||
|
@ -1092,24 +1393,6 @@
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isMSISDNFlowRequired
|
||||
{
|
||||
if (currentSession && currentSession.flows)
|
||||
{
|
||||
for (MXLoginFlow *loginFlow in currentSession.flows)
|
||||
{
|
||||
if ([loginFlow.stages indexOfObject:kMXLoginFlowTypeMSISDN] == NSNotFound && ![loginFlow.type isEqualToString:kMXLoginFlowTypeMSISDN])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isMSISDNFlowCompleted
|
||||
{
|
||||
if (currentSession && currentSession.completed)
|
||||
|
@ -1123,7 +1406,6 @@
|
|||
return NO;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isRecaptchaFlowRequired
|
||||
{
|
||||
if (currentSession && currentSession.flows)
|
||||
|
|
|
@ -127,13 +127,14 @@
|
|||
<constraints>
|
||||
<constraint firstAttribute="width" constant="44" id="Jm3-K6-0hG"/>
|
||||
</constraints>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="5" maxX="0.0" maxY="0.0"/>
|
||||
<state key="normal" image="shrink_icon.png"/>
|
||||
<connections>
|
||||
<action selector="selectPhoneNumberCountry:" destination="x74-04-ezp" eventType="touchUpInside" id="nvG-sc-8Wn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="FR" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u0g-Z1-e7a">
|
||||
<rect key="frame" x="13" y="14.5" width="30" height="21"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u0g-Z1-e7a">
|
||||
<rect key="frame" x="13" y="18" width="30" height="0.0"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="dTe-UY-tRF"/>
|
||||
</constraints>
|
||||
|
@ -141,23 +142,23 @@
|
|||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="584" translatesAutoresizingMaskIntoConstraints="NO" id="CLw-uO-q5T">
|
||||
<rect key="frame" x="60" y="14.5" width="42" height="21"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="584" translatesAutoresizingMaskIntoConstraints="NO" id="CLw-uO-q5T">
|
||||
<rect key="frame" x="60" y="18" width="10" height="0.0"/>
|
||||
<color key="tintColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="10" id="G1u-di-SRu"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="34" placeholder="Email address" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="4Md-Fk-tj9">
|
||||
<rect key="frame" x="110" y="18" width="472" height="21"/>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email address" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="4Md-Fk-tj9">
|
||||
<rect key="frame" x="78" y="18" width="504" height="21"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="AuthInputsViewEmailTextField"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="21" id="Atb-T3-6eG"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="phonePad" returnKeyType="done"/>
|
||||
<connections>
|
||||
<action selector="textFieldDidChange:" destination="x74-04-ezp" eventType="editingChanged" id="kR9-TL-ZDM"/>
|
||||
|
@ -177,17 +178,17 @@
|
|||
<constraints>
|
||||
<constraint firstItem="4Md-Fk-tj9" firstAttribute="leading" secondItem="CLw-uO-q5T" secondAttribute="trailing" constant="8" id="195-Hm-3Ue"/>
|
||||
<constraint firstItem="5r3-Tj-ARd" firstAttribute="top" secondItem="IB7-1E-eeL" secondAttribute="top" id="4gX-WV-3yS"/>
|
||||
<constraint firstItem="CLw-uO-q5T" firstAttribute="centerY" secondItem="IB7-1E-eeL" secondAttribute="centerY" id="ArE-5L-euR"/>
|
||||
<constraint firstItem="CLw-uO-q5T" firstAttribute="top" secondItem="IB7-1E-eeL" secondAttribute="top" constant="18" id="BOi-aC-RFk"/>
|
||||
<constraint firstItem="4Md-Fk-tj9" firstAttribute="top" secondItem="IB7-1E-eeL" secondAttribute="top" constant="18" id="Bac-nE-XY1"/>
|
||||
<constraint firstItem="0Uh-dj-10x" firstAttribute="leading" secondItem="IB7-1E-eeL" secondAttribute="leading" constant="10" id="CGT-Ho-HOv"/>
|
||||
<constraint firstItem="5r3-Tj-ARd" firstAttribute="leading" secondItem="IB7-1E-eeL" secondAttribute="leading" constant="8" id="KyO-Vy-ERT"/>
|
||||
<constraint firstItem="u0g-Z1-e7a" firstAttribute="leading" secondItem="5r3-Tj-ARd" secondAttribute="leading" constant="5" id="LwH-ff-U1C"/>
|
||||
<constraint firstAttribute="trailing" secondItem="0Uh-dj-10x" secondAttribute="trailing" constant="10" id="NYe-mf-ZBP"/>
|
||||
<constraint firstAttribute="bottom" secondItem="0Uh-dj-10x" secondAttribute="bottom" id="NhZ-6E-PKC"/>
|
||||
<constraint firstItem="u0g-Z1-e7a" firstAttribute="top" secondItem="IB7-1E-eeL" secondAttribute="top" constant="18" id="NtT-EB-z6J"/>
|
||||
<constraint firstAttribute="height" constant="50" id="Q5U-nJ-HaM"/>
|
||||
<constraint firstItem="CLw-uO-q5T" firstAttribute="leading" secondItem="5r3-Tj-ARd" secondAttribute="trailing" constant="8" id="kBc-p5-eJM"/>
|
||||
<constraint firstItem="5r3-Tj-ARd" firstAttribute="height" secondItem="IB7-1E-eeL" secondAttribute="height" id="nrv-aK-2kk"/>
|
||||
<constraint firstItem="u0g-Z1-e7a" firstAttribute="centerY" secondItem="5r3-Tj-ARd" secondAttribute="centerY" id="r7G-3L-YQc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4Md-Fk-tj9" secondAttribute="trailing" constant="18" id="wXN-hG-YNx"/>
|
||||
</constraints>
|
||||
</view>
|
||||
|
@ -227,7 +228,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="68j-f9-JG4">
|
||||
<rect key="frame" x="8" y="8" width="584" height="18"/>
|
||||
<rect key="frame" x="10" y="8" width="580" height="18"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="AuthInputsViewMessageLabel"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
|
@ -251,7 +252,7 @@
|
|||
<constraint firstItem="rb1-L5-udI" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" constant="100" id="75U-tx-PsQ"/>
|
||||
<constraint firstItem="UfH-jv-6w4" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="7Bk-GF-MZ0"/>
|
||||
<constraint firstItem="IB7-1E-eeL" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="7Lr-fy-W8L"/>
|
||||
<constraint firstAttribute="trailing" secondItem="68j-f9-JG4" secondAttribute="trailing" constant="8" id="8Aa-YT-MP5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="68j-f9-JG4" secondAttribute="trailing" constant="10" id="8Aa-YT-MP5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="UfH-jv-6w4" secondAttribute="trailing" id="8dz-wY-Kxx"/>
|
||||
<constraint firstItem="whs-Ob-uzD" firstAttribute="centerX" secondItem="x74-04-ezp" secondAttribute="centerX" id="8lX-k1-85c"/>
|
||||
<constraint firstItem="68j-f9-JG4" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" constant="8" id="BK1-XE-vz5"/>
|
||||
|
@ -261,7 +262,7 @@
|
|||
<constraint firstAttribute="trailing" secondItem="xOW-lo-QGC" secondAttribute="trailing" id="SNm-WQ-Piu"/>
|
||||
<constraint firstItem="xOW-lo-QGC" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" id="WmX-gO-hPJ"/>
|
||||
<constraint firstItem="rb1-L5-udI" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="XAJ-ST-sWV"/>
|
||||
<constraint firstItem="68j-f9-JG4" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" constant="8" id="aYh-VJ-bss"/>
|
||||
<constraint firstItem="68j-f9-JG4" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" constant="10" id="aYh-VJ-bss"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rb1-L5-udI" secondAttribute="trailing" id="c49-Cf-H9a"/>
|
||||
<constraint firstItem="bXz-VI-5FS" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" id="enV-j0-cgR"/>
|
||||
<constraint firstItem="whs-Ob-uzD" firstAttribute="top" secondItem="68j-f9-JG4" secondAttribute="bottom" id="g87-rp-bgb"/>
|
||||
|
|
Loading…
Reference in a new issue