Added initial integration with IdentityServer
This commit is contained in:
parent
7e9cbdee92
commit
d292646f3c
@ -0,0 +1,49 @@
|
||||
using System.Windows.Input;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace eShopOnContainers.Core.Behaviors
|
||||
{
|
||||
public class WebViewNavigationBehavior : Behavior<WebView>
|
||||
{
|
||||
private VisualElement _element;
|
||||
|
||||
public static readonly BindableProperty NavigateCommandProperty =
|
||||
BindableProperty.Create("NavigateCommand", typeof(ICommand),
|
||||
typeof(WebViewNavigationBehavior), default(ICommand),
|
||||
BindingMode.OneWay, null);
|
||||
|
||||
public ICommand NavigateCommand
|
||||
{
|
||||
get { return (ICommand)GetValue(NavigateCommandProperty); }
|
||||
set { SetValue(NavigateCommandProperty, value); }
|
||||
}
|
||||
|
||||
protected override void OnAttachedTo(WebView bindable)
|
||||
{
|
||||
_element = bindable;
|
||||
bindable.Navigating += OnWebViewNavigating;
|
||||
bindable.BindingContextChanged += OnBindingContextChanged;
|
||||
}
|
||||
|
||||
protected override void OnDetachingFrom(WebView bindable)
|
||||
{
|
||||
_element = null;
|
||||
BindingContext = null;
|
||||
bindable.Navigating -= OnWebViewNavigating;
|
||||
bindable.BindingContextChanged -= OnBindingContextChanged;
|
||||
}
|
||||
|
||||
private void OnBindingContextChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
BindingContext = _element?.BindingContext;
|
||||
}
|
||||
|
||||
private void OnWebViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||
{
|
||||
if (NavigateCommand != null && NavigateCommand.CanExecute(e.Url))
|
||||
{
|
||||
NavigateCommand.Execute(e.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@
|
||||
|
||||
public const string BasketEndpoint = "http://104.40.62.65:5103/";
|
||||
|
||||
public const string IdentityEndpoint = "http://104.40.62.65:5105/";
|
||||
public const string IdentityEndpoint = "http://104.40.62.65:5105/connect/authorize";
|
||||
|
||||
public const string LogoutEndpoint = "http://104.40.62.65:5105/connect/endsession";
|
||||
|
||||
public const string IdentityCallback = "http://localhost:5003/callback.html";
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace eShopOnContainers.Core.Services.Identity
|
||||
{
|
||||
public interface IIdentityService
|
||||
{
|
||||
string CreateAuthorizeRequest();
|
||||
string DecodeToken(string token);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
using IdentityModel.Client;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace eShopOnContainers.Core.Services.Identity
|
||||
{
|
||||
public class IdentityService : IIdentityService
|
||||
{
|
||||
public string CreateAuthorizeRequest()
|
||||
{
|
||||
// Create URI to authorize endpoint
|
||||
var authorizeRequest =
|
||||
new AuthorizeRequest(GlobalSetting.IdentityEndpoint);
|
||||
|
||||
// Dictionary with values for the authorize request
|
||||
var dic = new Dictionary<string, string>();
|
||||
dic.Add("client_id", "js");
|
||||
dic.Add("response_type", "id_token token");
|
||||
dic.Add("scope", "openid profile basket");
|
||||
|
||||
dic.Add("redirect_uri", GlobalSetting.IdentityCallback);
|
||||
dic.Add("nonce", Guid.NewGuid().ToString("N"));
|
||||
|
||||
// Add CSRF token to protect against cross-site request forgery attacks.
|
||||
var currentCSRFToken = Guid.NewGuid().ToString("N");
|
||||
dic.Add("state", currentCSRFToken);
|
||||
|
||||
var authorizeUri = authorizeRequest.Create(dic);
|
||||
|
||||
return authorizeUri;
|
||||
}
|
||||
|
||||
public string DecodeToken(string token)
|
||||
{
|
||||
var parts = token.Split('.');
|
||||
|
||||
string partToConvert = parts[1];
|
||||
partToConvert = partToConvert.Replace('-', '+');
|
||||
partToConvert = partToConvert.Replace('_', '/');
|
||||
switch (partToConvert.Length % 4)
|
||||
{
|
||||
case 0:
|
||||
break;
|
||||
case 2:
|
||||
partToConvert += "==";
|
||||
break;
|
||||
case 3:
|
||||
partToConvert += "=";
|
||||
break;
|
||||
}
|
||||
|
||||
var partAsBytes = Convert.FromBase64String(partToConvert);
|
||||
var partAsUTF8String = Encoding.UTF8.GetString(partAsBytes, 0, partAsBytes.Count());
|
||||
|
||||
return JObject.Parse(partAsUTF8String).ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using eShopOnContainers.Core.Services.OpenUrl;
|
||||
using eShopOnContainers.Core.Services.User;
|
||||
using eShopOnContainers.Core.Services.RequestProvider;
|
||||
using eShopOnContainers.Core.Services.Basket;
|
||||
using eShopOnContainers.Core.Services.Identity;
|
||||
|
||||
namespace eShopOnContainers.ViewModels.Base
|
||||
{
|
||||
@ -37,6 +38,7 @@ namespace eShopOnContainers.ViewModels.Base
|
||||
RegisterSingleton<INavigationService, NavigationService>();
|
||||
_unityContainer.RegisterType<IOpenUrlService, OpenUrlService>();
|
||||
_unityContainer.RegisterType<IRequestProvider, RequestProvider>();
|
||||
_unityContainer.RegisterType<IIdentityService, IdentityService>();
|
||||
|
||||
_unityContainer.RegisterType<ICatalogService, CatalogMockService>();
|
||||
_unityContainer.RegisterType<IBasketService, BasketMockService>();
|
||||
|
@ -1,6 +1,8 @@
|
||||
using eShopOnContainers.Core.Services.OpenUrl;
|
||||
using eShopOnContainers.Core.Services.Identity;
|
||||
using eShopOnContainers.Core.Services.OpenUrl;
|
||||
using eShopOnContainers.Core.Validations;
|
||||
using eShopOnContainers.ViewModels.Base;
|
||||
using IdentityModel.Client;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
@ -13,13 +15,18 @@ namespace eShopOnContainers.Core.ViewModels
|
||||
{
|
||||
private ValidatableObject<string> _userName;
|
||||
private ValidatableObject<string> _password;
|
||||
private bool _isMock;
|
||||
private bool _isValid;
|
||||
private string _loginUrl;
|
||||
|
||||
private IOpenUrlService _openUrlService;
|
||||
private IIdentityService _identityService;
|
||||
|
||||
public LoginViewModel(IOpenUrlService openUrlService)
|
||||
public LoginViewModel(IOpenUrlService openUrlService,
|
||||
IIdentityService identityService)
|
||||
{
|
||||
_openUrlService = openUrlService;
|
||||
_identityService = identityService;
|
||||
|
||||
_userName = new ValidatableObject<string>();
|
||||
_password = new ValidatableObject<string>();
|
||||
@ -53,6 +60,19 @@ namespace eShopOnContainers.Core.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMock
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isMock;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isMock = value;
|
||||
RaisePropertyChanged(() => IsMock);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
@ -66,11 +86,35 @@ namespace eShopOnContainers.Core.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public string LoginUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return _loginUrl;
|
||||
}
|
||||
set
|
||||
{
|
||||
_loginUrl = value;
|
||||
RaisePropertyChanged(() => LoginUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand MockSignInCommand => new Command(MockSignInAsync);
|
||||
|
||||
public ICommand SignInCommand => new Command(SignInAsync);
|
||||
|
||||
public ICommand RegisterCommand => new Command(Register);
|
||||
|
||||
private async void SignInAsync()
|
||||
public ICommand NavigateCommand => new Command<string>(NavigateAsync);
|
||||
|
||||
public override Task InitializeAsync(object navigationData)
|
||||
{
|
||||
LoginUrl = _identityService.CreateAuthorizeRequest();
|
||||
|
||||
return base.InitializeAsync(navigationData);
|
||||
}
|
||||
|
||||
private async void MockSignInAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
IsValid = true;
|
||||
@ -104,11 +148,42 @@ namespace eShopOnContainers.Core.ViewModels
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
private async void SignInAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
IsValid = true;
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
private void Register()
|
||||
{
|
||||
_openUrlService.OpenUrl(GlobalSetting.RegisterWebsite);
|
||||
}
|
||||
|
||||
private async void NavigateAsync(string url)
|
||||
{
|
||||
if (url.Contains(GlobalSetting.IdentityCallback))
|
||||
{
|
||||
// Parse response
|
||||
var authResponse = new AuthorizeResponse(url);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authResponse.AccessToken))
|
||||
{
|
||||
string decodedTokens = _identityService.DecodeToken(authResponse.AccessToken);
|
||||
|
||||
if(decodedTokens != null)
|
||||
{
|
||||
await NavigationService.NavigateToAsync<MainViewModel>();
|
||||
await NavigationService.RemoveLastFromBackStackAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool Validate()
|
||||
{
|
||||
bool isValidUser = _userName.Validate();
|
||||
|
@ -3,7 +3,8 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="eShopOnContainers.Core.Views.LoginView"
|
||||
xmlns:animations="clr-namespace:eShopOnContainers.Core.Animations;assembly=eShopOnContainers.Core"
|
||||
xmlns:triggers="clr-namespace:eShopOnContainers.Core.Triggers;assembly=eShopOnContainers.Core"
|
||||
xmlns:triggers="clr-namespace:eShopOnContainers.Core.Triggers;assembly=eShopOnContainers.Core"
|
||||
xmlns:behaviors="clr-namespace:eShopOnContainers.Core.Behaviors;assembly=eShopOnContainers.Core"
|
||||
Title="eShop on Containers">
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
@ -46,6 +47,20 @@
|
||||
Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="LoginPanelStyle"
|
||||
TargetType="{x:Type Grid}">
|
||||
<Setter Property="HeightRequest"
|
||||
Value="60" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{StaticResource LightGreenColor}" />
|
||||
<Setter Property="HorizontalOptions"
|
||||
Value="FillAndExpand" />
|
||||
<Setter Property="VerticalOptions"
|
||||
Value="Center" />
|
||||
<Setter Property="Margin"
|
||||
Value="12" />
|
||||
</Style>
|
||||
|
||||
<animations:StoryBoard
|
||||
x:Key="LoginAnimation"
|
||||
Target="{x:Reference LoginPanel}">
|
||||
@ -65,8 +80,10 @@
|
||||
</ContentPage.Triggers>
|
||||
<Grid
|
||||
BackgroundColor="{StaticResource BackgroundColor}">
|
||||
<!-- MOCK AUTH -->
|
||||
<Grid
|
||||
x:Name="LoginPanel"
|
||||
x:Name="LoginPanel"
|
||||
IsVisible="{Binding IsMock}"
|
||||
Padding="0"
|
||||
ColumnSpacing="0"
|
||||
RowSpacing="0">
|
||||
@ -154,6 +171,22 @@
|
||||
Padding="0"
|
||||
ColumnSpacing="0"
|
||||
RowSpacing="0">
|
||||
<Label
|
||||
Text="[ LOGIN ]"
|
||||
Style="{StaticResource LoginButtonStyle}"/>
|
||||
<Grid.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding MockSignInCommand}"
|
||||
NumberOfTapsRequired="1" />
|
||||
</Grid.GestureRecognizers>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<!-- AUTH -->
|
||||
<Grid
|
||||
IsVisible="{Binding IsMock, Converter={StaticResource InverseBoolConverter}}">
|
||||
<!-- LOGIN BUTTON -->
|
||||
<Grid
|
||||
Style="{StaticResource LoginPanelStyle}">
|
||||
<Label
|
||||
Text="[ LOGIN ]"
|
||||
Style="{StaticResource LoginButtonStyle}"/>
|
||||
@ -163,6 +196,20 @@
|
||||
NumberOfTapsRequired="1" />
|
||||
</Grid.GestureRecognizers>
|
||||
</Grid>
|
||||
<!-- WEBVIEW -->
|
||||
<AbsoluteLayout
|
||||
IsVisible="{Binding IsValid}">
|
||||
<WebView
|
||||
x:Name="AuthWebView"
|
||||
Source="{Binding LoginUrl}"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All">
|
||||
<WebView.Behaviors>
|
||||
<behaviors:WebViewNavigationBehavior
|
||||
NavigateCommand="{Binding NavigateCommand}"/>
|
||||
</WebView.Behaviors>
|
||||
</WebView>
|
||||
</AbsoluteLayout>
|
||||
</Grid>
|
||||
<!-- INDICATOR -->
|
||||
<ActivityIndicator
|
||||
|
@ -8,5 +8,24 @@ namespace eShopOnContainers.Core.Views
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
AuthWebView.Navigating += OnAuthWebViewNavigating;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
AuthWebView.Navigating -= OnAuthWebViewNavigating;
|
||||
}
|
||||
|
||||
private void OnAuthWebViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
</Compile>
|
||||
<Compile Include="Behaviors\Base\BindableBehavior.cs" />
|
||||
<Compile Include="Behaviors\EventToCommandBehavior.cs" />
|
||||
<Compile Include="Behaviors\WebViewNavigationBehavior.cs" />
|
||||
<Compile Include="Controls\BindablePicker.cs" />
|
||||
<Compile Include="Controls\AddBasketButton.xaml.cs">
|
||||
<DependentUpon>AddBasketButton.xaml</DependentUpon>
|
||||
@ -80,6 +81,8 @@
|
||||
<Compile Include="Services\Catalog\CatalogService.cs" />
|
||||
<Compile Include="Services\Dialog\DialogService.cs" />
|
||||
<Compile Include="Services\Dialog\IDialogService.cs" />
|
||||
<Compile Include="Services\Identity\IdentityService.cs" />
|
||||
<Compile Include="Services\Identity\IIdentityService.cs" />
|
||||
<Compile Include="Services\Navigation\INavigationService.cs" />
|
||||
<Compile Include="Services\Navigation\NavigationService.cs" />
|
||||
<Compile Include="Services\OpenUrl\IOpenUrlService.cs" />
|
||||
@ -186,6 +189,10 @@
|
||||
<HintPath>..\..\packages\Xamarin.FFImageLoading.2.2.6-pre-232\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Platform.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="IdentityModel, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\IdentityModel.1.13.1\lib\portable-net45+wp80+win8+wpa81\IdentityModel.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
|
@ -2,6 +2,7 @@
|
||||
<packages>
|
||||
<package id="Acr.UserDialogs" version="6.3.1" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
<package id="CommonServiceLocator" version="1.3" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
<package id="IdentityModel" version="1.13.1" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="portable45-net45+win8+wp8+wpa81" />
|
||||
|
@ -17,7 +17,7 @@
|
||||
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
|
||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||
<AndroidUseLatestPlatformSdk>true</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v6.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v7.0</TargetFrameworkVersion>
|
||||
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
|
||||
<AndroidStoreUncompressedFileExtensions />
|
||||
<MandroidI18n />
|
||||
|
@ -17,7 +17,7 @@
|
||||
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
|
||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||
<AndroidUseLatestPlatformSdk>true</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v7.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v6.0</TargetFrameworkVersion>
|
||||
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
|
||||
<AndroidStoreUncompressedFileExtensions />
|
||||
<MandroidI18n />
|
||||
@ -223,6 +223,8 @@
|
||||
<None Include="packages.config" />
|
||||
<None Include="Resources\AboutResources.txt" />
|
||||
<None Include="Assets\AboutAssets.txt" />
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\switch_off.png" />
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\switch_on.png" />
|
||||
<AndroidResource Include="Resources\layout\Tabs.axml">
|
||||
<SubType>Designer</SubType>
|
||||
</AndroidResource>
|
||||
@ -332,9 +334,6 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\switch_on.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\switch_on.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\switch_off.png" />
|
||||
</ItemGroup>
|
||||
@ -342,10 +341,10 @@
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\switch_off.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\switch_off.png" />
|
||||
<AndroidResource Include="Resources\drawable\noimage.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\noimage.png" />
|
||||
<AndroidResource Include="Resources\drawable\default_product.png" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" />
|
||||
@ -354,9 +353,7 @@
|
||||
<ErrorText>Este proyecto hace referencia a los paquetes NuGet que faltan en este equipo. Use la restauración de paquetes NuGet para descargarlos. Para obtener más información, consulte http://go.microsoft.com/fwlink/?LinkID=322105. El archivo que falta es {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\StyleCop.MSBuild.5.0.0-alpha01\build\StyleCop.MSBuild.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\StyleCop.MSBuild.5.0.0-alpha01\build\StyleCop.MSBuild.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Xamarin.Forms.2.3.2.127\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Xamarin.Forms.2.3.2.127\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\..\packages\StyleCop.MSBuild.5.0.0-alpha01\build\StyleCop.MSBuild.targets" Condition="Exists('..\..\packages\StyleCop.MSBuild.5.0.0-alpha01\build\StyleCop.MSBuild.targets')" />
|
||||
<Import Project="..\..\packages\Xamarin.Forms.2.3.2.127\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets" Condition="Exists('..\..\packages\Xamarin.Forms.2.3.2.127\build\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.targets')" />
|
||||
</Project>
|
Loading…
x
Reference in New Issue
Block a user