<|start_filename|>TurnipTracker.Functions/Model/ProStatusEntity.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.WindowsAzure.Storage.Table;

namespace TurnipTracker.Functions.Model
{
    public class ProStatusEntity : TableEntity
    {
        public ProStatusEntity()
        {

        }
        public ProStatusEntity(string publicKey, string privateKey)
        {
            PartitionKey = publicKey;
            RowKey = privateKey;
        }

        public string Receipt { get; set; }
    }
}


<|start_filename|>UserSettings/UserSettings/Form1.Designer.cs<|end_filename|>
﻿namespace UserSettings
{
    partial class Form1
    {
        /// <summary>
        ///  Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        ///  Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        ///  Required method for Designer support - do not modify
        ///  the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.label1 = new System.Windows.Forms.Label();
            this.label2 = new System.Windows.Forms.Label();
            this.TextBoxPrivate = new System.Windows.Forms.TextBox();
            this.TextBoxPublic = new System.Windows.Forms.TextBox();
            this.TextBoxEncoded = new System.Windows.Forms.TextBox();
            this.TextBoxExported = new System.Windows.Forms.TextBox();
            this.TextBoxDecodedPrivate = new System.Windows.Forms.TextBox();
            this.label3 = new System.Windows.Forms.Label();
            this.label4 = new System.Windows.Forms.Label();
            this.label5 = new System.Windows.Forms.Label();
            this.ButtonEncode = new System.Windows.Forms.Button();
            this.ButtonDecode = new System.Windows.Forms.Button();
            this.TextBoxDecodedPublic = new System.Windows.Forms.TextBox();
            this.label6 = new System.Windows.Forms.Label();
            this.label7 = new System.Windows.Forms.Label();
            this.SuspendLayout();
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(12, 9);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(46, 15);
            this.label1.TabIndex = 0;
            this.label1.Text = "Encode";
            // 
            // label2
            // 
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(12, 161);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(47, 15);
            this.label2.TabIndex = 1;
            this.label2.Text = "Decode";
            // 
            // TextBoxPrivate
            // 
            this.TextBoxPrivate.Location = new System.Drawing.Point(66, 28);
            this.TextBoxPrivate.Name = "TextBoxPrivate";
            this.TextBoxPrivate.Size = new System.Drawing.Size(394, 23);
            this.TextBoxPrivate.TabIndex = 2;
            // 
            // TextBoxPublic
            // 
            this.TextBoxPublic.Location = new System.Drawing.Point(66, 57);
            this.TextBoxPublic.Name = "TextBoxPublic";
            this.TextBoxPublic.Size = new System.Drawing.Size(394, 23);
            this.TextBoxPublic.TabIndex = 3;
            // 
            // TextBoxEncoded
            // 
            this.TextBoxEncoded.Location = new System.Drawing.Point(66, 86);
            this.TextBoxEncoded.Name = "TextBoxEncoded";
            this.TextBoxEncoded.Size = new System.Drawing.Size(394, 23);
            this.TextBoxEncoded.TabIndex = 4;
            // 
            // TextBoxExported
            // 
            this.TextBoxExported.Location = new System.Drawing.Point(66, 184);
            this.TextBoxExported.Name = "TextBoxExported";
            this.TextBoxExported.Size = new System.Drawing.Size(394, 23);
            this.TextBoxExported.TabIndex = 5;
            // 
            // TextBoxDecodedPrivate
            // 
            this.TextBoxDecodedPrivate.Location = new System.Drawing.Point(66, 213);
            this.TextBoxDecodedPrivate.Name = "TextBoxDecodedPrivate";
            this.TextBoxDecodedPrivate.Size = new System.Drawing.Size(394, 23);
            this.TextBoxDecodedPrivate.TabIndex = 6;
            // 
            // label3
            // 
            this.label3.AutoSize = true;
            this.label3.Location = new System.Drawing.Point(22, 31);
            this.label3.Name = "label3";
            this.label3.Size = new System.Drawing.Size(43, 15);
            this.label3.TabIndex = 7;
            this.label3.Text = "Private";
            // 
            // label4
            // 
            this.label4.AutoSize = true;
            this.label4.Location = new System.Drawing.Point(22, 60);
            this.label4.Name = "label4";
            this.label4.Size = new System.Drawing.Size(40, 15);
            this.label4.TabIndex = 8;
            this.label4.Text = "Public";
            // 
            // label5
            // 
            this.label5.AutoSize = true;
            this.label5.Location = new System.Drawing.Point(8, 187);
            this.label5.Name = "label5";
            this.label5.Size = new System.Drawing.Size(54, 15);
            this.label5.TabIndex = 9;
            this.label5.Text = "Exported";
            // 
            // ButtonEncode
            // 
            this.ButtonEncode.Location = new System.Drawing.Point(16, 85);
            this.ButtonEncode.Name = "ButtonEncode";
            this.ButtonEncode.Size = new System.Drawing.Size(44, 23);
            this.ButtonEncode.TabIndex = 10;
            this.ButtonEncode.Text = "Go";
            this.ButtonEncode.UseVisualStyleBackColor = true;
            this.ButtonEncode.Click += new System.EventHandler(this.ButtonEncode_Click);
            // 
            // ButtonDecode
            // 
            this.ButtonDecode.Location = new System.Drawing.Point(12, 274);
            this.ButtonDecode.Name = "ButtonDecode";
            this.ButtonDecode.Size = new System.Drawing.Size(44, 23);
            this.ButtonDecode.TabIndex = 11;
            this.ButtonDecode.Text = "Go";
            this.ButtonDecode.UseVisualStyleBackColor = true;
            this.ButtonDecode.Click += new System.EventHandler(this.ButtonDecode_Click);
            // 
            // TextBoxDecodedPublic
            // 
            this.TextBoxDecodedPublic.Location = new System.Drawing.Point(66, 242);
            this.TextBoxDecodedPublic.Name = "TextBoxDecodedPublic";
            this.TextBoxDecodedPublic.Size = new System.Drawing.Size(394, 23);
            this.TextBoxDecodedPublic.TabIndex = 12;
            // 
            // label6
            // 
            this.label6.AutoSize = true;
            this.label6.Location = new System.Drawing.Point(15, 221);
            this.label6.Name = "label6";
            this.label6.Size = new System.Drawing.Size(43, 15);
            this.label6.TabIndex = 13;
            this.label6.Text = "Private";
            // 
            // label7
            // 
            this.label7.AutoSize = true;
            this.label7.Location = new System.Drawing.Point(16, 245);
            this.label7.Name = "label7";
            this.label7.Size = new System.Drawing.Size(40, 15);
            this.label7.TabIndex = 14;
            this.label7.Text = "Public";
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(483, 309);
            this.Controls.Add(this.label7);
            this.Controls.Add(this.label6);
            this.Controls.Add(this.TextBoxDecodedPublic);
            this.Controls.Add(this.ButtonDecode);
            this.Controls.Add(this.ButtonEncode);
            this.Controls.Add(this.label5);
            this.Controls.Add(this.label4);
            this.Controls.Add(this.label3);
            this.Controls.Add(this.TextBoxDecodedPrivate);
            this.Controls.Add(this.TextBoxExported);
            this.Controls.Add(this.TextBoxEncoded);
            this.Controls.Add(this.TextBoxPublic);
            this.Controls.Add(this.TextBoxPrivate);
            this.Controls.Add(this.label2);
            this.Controls.Add(this.label1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private Label label1;
        private Label label2;
        private TextBox TextBoxPrivate;
        private TextBox TextBoxPublic;
        private TextBox TextBoxEncoded;
        private TextBox TextBoxExported;
        private TextBox TextBoxDecodedPrivate;
        private Label label3;
        private Label label4;
        private Label label5;
        private Button ButtonEncode;
        private Button ButtonDecode;
        private TextBox TextBoxDecodedPublic;
        private Label label6;
        private Label label7;
    }
}

<|start_filename|>TurnipTracker/Model/Fruit.cs<|end_filename|>
﻿using System;
namespace TurnipTracker.Model
{
    public enum Fruit
    {
        Apple = 0,
        Cherry = 1,
        Orange = 2,
        Peach = 3,
        Pear = 4
    }

    public class FruitItem
    {
        public string Name { get; set; }
        public string Icon { get; set; }
    }
}


<|start_filename|>TurnipTracker/View/ProfilePage.xaml.cs<|end_filename|>
﻿using TurnipTracker.ViewModel;
using Xamarin.Forms;

namespace TurnipTracker.View
{
    public partial class ProfilePage : ContentPage
    {
        ProfileViewModel vm;
        public ProfilePage()
        {
            InitializeComponent();
            BindingContext = vm = new ProfileViewModel();
        }

        protected override bool OnBackButtonPressed()
        {
            if (vm.IsBusy)
                return false;

            return base.OnBackButtonPressed();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();

            ComboBoxFruit.SelectedItem = ComboBoxFruit.ComboBoxSource[vm.Profile.Fruit];
        }
    }
}


<|start_filename|>TurnipTracker/Services/DataService.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using MonkeyCache;
using MonkeyCache.FileStore;
using Newtonsoft.Json;
using TurnipTracker.Model;
using TurnipTracker.Services;
using TurnipTracker.Shared;
using Xamarin.Essentials;
using Xamarin.Forms;

[assembly: Dependency(typeof(DataService))]
namespace TurnipTracker.Services
{
    public class HttpResponseException : Exception
    {
        public HttpStatusCode StatusCode { get; private set; }

        public HttpResponseException(HttpStatusCode statusCode, string content) : base(content)
        {
            StatusCode = statusCode;
        }
    }
    public class DataService
    {
        IBarrel barrel;
        object locker = new object();
        CalendarWeekRule myCWR;
        DayOfWeek myFirstDOW;
        CultureInfo myCI;

        readonly HttpClient client;

        public  DataService()
        {
            client = new HttpClient()
            {
                BaseAddress = new Uri(App.BaseUrl)
            };

            Barrel.ApplicationId = AppInfo.PackageName;
            barrel = Barrel.Create(FileSystem.AppDataDirectory);
            myCI = new CultureInfo("en-US");
            myCWR = myCI.DateTimeFormat.CalendarWeekRule;
            myFirstDOW = myCI.DateTimeFormat.FirstDayOfWeek;
        }

        public Profile GetProfile()
        {
            lock (locker)
            {
                var profile = barrel.Get<Profile>("profile");
                profile ??= new Profile
                {
                    Fruit = (int)Model.Fruit.Apple,
                    Status = "😍",
                    Name = string.Empty,
                    FriendCode = string.Empty,
                    IslandName = string.Empty,
                    GateClosesAtUTC = DateTime.UtcNow,
                    DodoCode = string.Empty,
                    GateStatus = (int)Model.GateStatus.Closed,
                    GatesOpenLength = .5,
                    TimeZone = string.Empty
                };

                //if(profile.GateStatus != (int)GateStatus.Closed && profile.GateClosesAtUTC < DateTime.UtcNow)
                //{
                //    profile.GateStatus = (int)GateStatus.Closed;
                //    profile.DodoCode = string.Empty;
                //    profile.GatesOpenLength = .5;
                //}

                return profile;
            }
        }

        public void SaveProfile(Profile profile)
        {
            lock(locker)
            {
                barrel.Add<Profile>("profile", profile, TimeSpan.FromDays(365));
            }
        }

        public int GetWeekOfYear() =>
            myCI.Calendar.GetWeekOfYear(DateTime.Now, myCWR, myFirstDOW);

        string GetCurrentWeekKey() =>
            $"week_{GetWeekOfYear()}_{DateTime.Now.Year}";

        public List<Day> GetCurrentWeek()
        {
            lock (locker)
            {
                var days = barrel.Get<List<Day>>(GetCurrentWeekKey());

                return days ?? new List<Day>
                {
                    new Day { DayLong = "Sunday", IsSunday = true},
                    new Day { DayLong = "Monday" },
                    new Day { DayLong = "Tuesday" },
                    new Day { DayLong = "Wednesday" },
                    new Day { DayLong = "Thursday" },
                    new Day { DayLong = "Friday" },
                    new Day { DayLong = "Saturday" }
                };
            }
        }

        public async Task CreateProStatus(ProStatus status)
        {
            status.PublicKey = await SettingsService.GetPublicKey();            
            var content = JsonConvert.SerializeObject(status);
            await PostAsync($"api/CreateProStatus?code={App.PostProStatusKey}", content);            
        }

        public async Task UpsertUserProfile(Profile profile)
        {
            profile ??= GetProfile();

            var user = new User
            {
                Fruit = profile.Fruit,
                IslandName = profile.IslandName,
                Name = profile.Name,
                Status = profile.Status ?? string.Empty,
                TimeZone = profile.TimeZone ?? string.Empty,
                FriendCode = profile.FriendCode ?? string.Empty,
                DodoCode = profile.DodoCode ?? string.Empty,
                GateStatus = profile.GateStatus,
                GateClosesAtUTC = profile.GateClosesAtUTC
            };
            user.PublicKey = await SettingsService.GetPublicKey();
            if (SettingsService.HasRegistered)
            {
                var content = JsonConvert.SerializeObject(user);
                await PutAsync($"api/UpdateProfile?code={App.PutUpdateProfileKey}", content);
            }
            else
            {                
                var content = JsonConvert.SerializeObject(user);
                await PostAsync($"api/CreateProfile?code={App.PostCreateProfileKey}", content);
                SettingsService.HasRegistered = true;
            }
        }

        public async Task UpdateTurnipPrices(Day day, int min, int max)
        {
            day ??= GetCurrentWeek()[(int)DateTime.Now.DayOfWeek];
            var key = await SettingsService.GetPublicKey();
            var turnipUpdate = new TurnipUpdate
            {
                AMPrice = day.PriceAM ?? 0,
                PMPrice = day.PricePM ?? 0,
                BuyPrice = day.BuyPrice ?? 0,
                Year = DateTime.Now.Year,
                DayOfYear = DateTime.Now.DayOfYear,
                PublicKey = key,
                TurnipUpdateTimeUTC = DateTime.UtcNow,
                MaxPrediction = max,
                MinPrediction = min
            };

            var content = JsonConvert.SerializeObject(turnipUpdate);

            
            await PutAsync($"api/UpdateTurnipPrices?code={App.PutUpdateTurnipPricesKey}", content);
        }

        public async Task SubmitFriendRequestAsync(string requesterKey)
        {
            var publicKey = await SettingsService.GetPublicKey();
            var content = JsonConvert.SerializeObject(new FriendRequest { MyPublicKey = publicKey, FriendPublicKey = requesterKey, ProUser = SettingsService.IsPro });


            await PostAsync($"api/SubmitFriendRequest?code={App.PostSubmitFriendRequestKey}", content);
        }

        public async Task RemoveFriendAsync(string friendToRemoveKey)
        {
            var publicKey = await SettingsService.GetPublicKey();
            await DeleteAsync($"api/RemoveFriend/{publicKey}/{friendToRemoveKey}?code={App.DeleteRemoveFriendKey}");
        }

        public async Task RemoveFriendRequestAsync(string friendRequestToRemoveKey)
        {
            var publicKey = await SettingsService.GetPublicKey();
            var content = JsonConvert.SerializeObject(new FriendRequest { MyPublicKey = publicKey, FriendPublicKey = friendRequestToRemoveKey });
            await PostAsync($"api/RejectFriendRequest?code={App.PostRemoveFriendRequestKey}", content);
        }

        public async Task ApproveFriendRequestAsync(string friendToApproveKey)
        {
            var publicKey = await SettingsService.GetPublicKey();
            var content = JsonConvert.SerializeObject(new FriendRequest { MyPublicKey = publicKey, FriendPublicKey = friendToApproveKey, ProUser = SettingsService.IsPro });
            await PostAsync($"api/ApproveFriendRequest?code={App.PostApproveFriendRequestKey}", content);
        }

        public const string FriendKey = "get_friends";
        public const string ProStatusKey = "get_pro_status";
        public const string FriendRequestKey = "get_friend_requests";
        public const string FriendRequestCountKey = "get_friend_requests_count";

        public void ClearCache(string key) =>
            Barrel.Current.Empty(key);

        public T GetCache<T>(string key)
        {
            if (!Barrel.Current.Exists(key))
                return default;

            var json = Barrel.Current.Get<string>(key);
            
            return JsonConvert.DeserializeObject<T>(json);

        }

        public async Task<FriendRequestCount> GetFriendRequestCountAsync(bool forceRefresh = false)
        {
            var publicKey = await SettingsService.GetPublicKey();

            return await GetAsync<FriendRequestCount>($"api/GetFriendRequestCount/{publicKey}?code={App.GetFriendRequestCountKey}", FriendRequestCountKey, 1, forceRefresh);
        }

        public async Task<IEnumerable<FriendStatus>> GetFriendsAsync(bool forceRefresh = false)
        {
            var publicKey = await SettingsService.GetPublicKey();

            return await GetAsync<IEnumerable<FriendStatus>>($"api/GetFriends/{publicKey}?code={App.GetFriendsKey}", FriendKey, 1, forceRefresh);
        }

        public async Task<ProStatus> GetProStatus()
        {
            var publicKey = await SettingsService.GetPublicKey();

            return await GetAsync<ProStatus>($"api/GetProStatus/{publicKey}?code={App.GetProStatusKey}", ProStatusKey, 0, true);
        }

        public async Task<IEnumerable<PendingFriendRequest>> GetFriendRequestsAsync(bool forceRefresh = false)
        {
            var publicKey = await SettingsService.GetPublicKey();

            return await GetAsync<IEnumerable<PendingFriendRequest>>($"api/GetFriendRequests/{publicKey}?code={App.GetFriendRequestsKey}", FriendRequestKey, 1, forceRefresh);
        }
        public void SaveCurrentWeek(List<Day> days)
        {
            lock(locker)
            {
                barrel.Add<List<Day>>(GetCurrentWeekKey(), days, TimeSpan.FromDays(7));
            }
        }

        async Task SetHeader()
        {
            if (client.DefaultRequestHeaders.Authorization == null || SettingsService.TransferedIn)
            {
                SettingsService.TransferedIn = false;
                var key = await SettingsService.GetPrivateKey();
                var encoding = Encoding.GetEncoding("iso-8859-1");
                var authenticationBytes = encoding.GetBytes(key);
                var token = Convert.ToBase64String(authenticationBytes);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
            }
        }

        async Task DeleteAsync(string url)
        {
            await SetHeader();
            
            using var response = await client.DeleteAsync(url);

            using var responseContent = response.Content;
            var responseString = await responseContent.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                if (responseString?.StartsWith("{\"Message") ?? false)
                    responseString = JsonConvert.DeserializeObject<HttpMessage>(responseString)?.Message ?? responseString;
                throw new HttpResponseException(response.StatusCode, responseString);
            }
        }

        async Task PutAsync(string url, string content)
        {
            await SetHeader();

            using var response = await client.PutAsync(url, new StringContent(content, Encoding.UTF8, "application/json"));

            using var responseContent = response.Content;
            var responseString = await responseContent.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                if (responseString?.StartsWith("{\"Message") ?? false)
                    responseString = JsonConvert.DeserializeObject<HttpMessage>(responseString)?.Message ?? responseString;
                throw new HttpResponseException(response.StatusCode, responseString);
            }
        }

        async Task PostAsync(string url, string content)
        {
            await SetHeader();

            using var response = await client.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"));

            using var responseContent = response.Content;
            var responseString = await responseContent.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                if (responseString?.StartsWith("{\"Message") ?? false)
                    responseString = JsonConvert.DeserializeObject<HttpMessage>(responseString)?.Message ?? responseString;
                throw new HttpResponseException(response.StatusCode, responseString);
            }
        }

        async Task<T> PostAsync<T>(string url, string content)
        {
            await SetHeader();

            using var response = await client.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"));

            using var responseContent = response.Content;
            var responseString = await responseContent.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                if (responseString?.StartsWith("{\"Message") ?? false)
                    responseString = JsonConvert.DeserializeObject<HttpMessage>(responseString)?.Message ?? responseString;
                throw new HttpResponseException(response.StatusCode, responseString);
            }

            return JsonConvert.DeserializeObject<T>(responseString);
        }

        async Task<T> GetAsync<T>(string url, string key, int mins = 1, bool forceRefresh = false)
        {
            var json = string.Empty;

            if (Connectivity.NetworkAccess != NetworkAccess.Internet)
                json = Barrel.Current.Get<string>(key);
            else if (!forceRefresh && !Barrel.Current.IsExpired(key))
                json = Barrel.Current.Get<string>(key);

            try
            {
                if (string.IsNullOrWhiteSpace(json))
                {
                    await SetHeader();
                    
                    json = await client.GetStringAsync(url);
 
                    Barrel.Current.Add(key, json, TimeSpan.FromMinutes(mins));
                }
                return JsonConvert.DeserializeObject<T>(json);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Unable to get information from server {ex}");
                throw ex;
            }
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Turnips/GetFriends.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using TurnipTracker.Functions.Helpers;
using System.Net.Http;
using System.Net;
using TurnipTracker.Shared;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace TurnipTracker.Functions
{
    public static class GetFriends
    {
        [FunctionName(nameof(GetFriends))]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetFriends/{myPublicKey}")] HttpRequest req,
            string myPublicKey,
            [Table("Friend")] CloudTable friendTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(GetFriends)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new HttpResponseMessage(HttpStatusCode.Unauthorized)
                {
                    Content = new StringContent("You are not authorized to make this request.")
                };

            if (string.IsNullOrWhiteSpace(myPublicKey))
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Invalid data to process request")
                };
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, myPublicKey);
                if (user == null)
                    return new HttpResponseMessage(HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Unable to locate your user account.")
                    };
            }
            catch (Exception ex)
            {
                log.LogInformation("User doesn't exist: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Unable to locate your user account.")
                };
            }


            var statuses = new List<FriendStatus>();
            try
            {
                var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey",
                    QueryComparisons.Equal, myPublicKey);

                var rangeQuery = new TableQuery<FriendEntity>().Where(publicKeyFilter);

                var friends = await friendTable.ExecuteQuerySegmentedAsync(rangeQuery, null);

                // spin off tasks for finding all friends
                var tasks = new List<Task<TableQuerySegment<UserEntity>>>();
                foreach(var f in friends)
                {
                    tasks.Add(Utils.FindFriendTask(userTable, f.RequesteePublicKey));
                }

                await Task.WhenAll(tasks);

                foreach (var t in tasks)
                {
                    var friend = t.Result?.Results?.FirstOrDefault();
                    if (friend == null)
                        continue;

                    var friendCode = string.Empty;
                    try
                    {
                        friendCode = Cipher.Decrypt(friend.FriendCode, Utils.FriendCodePassword, friend.PublicKey);
                    }
                    catch (Exception ex)
                    {
                        log.LogError("Unable to decrypt friendcode: " + ex.Message);
                    }

                    statuses.Add(new FriendStatus
                    {
                        AMPrice = friend.AMPrice,
                        BuyPrice = friend.BuyPrice,
                        Fruit = friend.Fruit,
                        IslandName = friend.IslandName,
                        Name = friend.Name,
                        PMPrice = friend.PMPrice,
                        PublicKey = friend.PublicKey,
                        Status = friend.Status,
                        TimeZone = friend.TimeZone,
                        TurnipUpdateDayOfYear = friend.TurnipUpdateDayOfYear,
                        TurnipUpdateTimeUTC = friend.TurnipUpdateTimeUTC,
                        TurnipUpdateYear = friend.TurnipUpdateYear,
                        FriendCode = friendCode,
                        MaxPrediction = friend.MaxPrediction,
                        MinPrediction = friend.MinPrediction,
                        DodoCode = friend.DodoCode ?? string.Empty,
                        GateClosesAtUTC = friend.GateClosesAtUTC.HasValue ? friend.GateClosesAtUTC.Value : DateTime.UtcNow,
                        GateStatus = friend.GateStatus
                    });
                }

            }
            catch (Exception ex)
            {
                log.LogError($"Error {nameof(GetFriends)} - Error: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }

            var json = JsonConvert.SerializeObject(statuses);
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };
        }
    }
}


<|start_filename|>TurnipTracker/Helpers/ViewHelpers.cs<|end_filename|>
﻿using System;
using Xamarin.Forms;

namespace TurnipTracker.Helpers
{
    public static class ViewHelpers
    {
        public static Rectangle GetAbsoluteBounds(this Xamarin.Forms.View element)
        {
            Element looper = element;

            var absoluteX = element.X + element.Margin.Top;
            var absoluteY = element.Y + element.Margin.Left;

            // TODO: add logic to handle titles, headers, or other non-view bars

            while (looper.Parent != null)
            {
                looper = looper.Parent;
                if (looper is Xamarin.Forms.View v)
                {
                    absoluteX += v.X + v.Margin.Top;
                    absoluteY += v.Y + v.Margin.Left;
                }
            }

            return new Rectangle(absoluteX, absoluteY, element.Width, element.Height);
        }

        public static System.Drawing.Rectangle ToSystemRectangle(this Rectangle rect) =>
            new System.Drawing.Rectangle((int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height);
    }
}


<|start_filename|>TurnipTracker.Functions/FriendRequests/GetFriendRequestCount.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Web.Http;
using TurnipTracker.Shared;
using TurnipTracker.Functions.Helpers;
using System.Collections.Generic;
using System.Net.Http;
using System.Net;
using System.Text;
using System.Linq;

namespace TurnipTracker.Functions
{
    public static class GetFriendRequestCount
    {
        [FunctionName(nameof(GetFriendRequestCount))]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetFriendRequestCount/{myPublicKey}")] HttpRequest req,
            string myPublicKey,
            [Table("FriendRequest")] CloudTable friendRequestTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(GetFriendRequestCount)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new HttpResponseMessage(HttpStatusCode.Unauthorized)
                {
                    Content = new StringContent("You are not authorized to make this request.")
                };

            if (string.IsNullOrWhiteSpace(myPublicKey))
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Invalid data to process request")
                };
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, myPublicKey);
                if (user == null)
                    return new HttpResponseMessage(HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Unable to locate your user account.")
                    };
            }
            catch (Exception ex)
            {
                log.LogInformation("User doesn't exist: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Unable to locate your user account.")
                };
            }


            var requests = new FriendRequestCount();
            try
            {
                var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", 
                    QueryComparisons.Equal, myPublicKey);

                var rangeQuery = new TableQuery<FriendRequestEntity>().Where(publicKeyFilter).Select(new List<string> { "PartitionKey", "RowKey", "Timestamp" });

                var existingFriendRequests = await friendRequestTable.ExecuteQuerySegmentedAsync(rangeQuery, null);

                requests.Count =  existingFriendRequests.Count();

            }
            catch (Exception ex)
            {
                log.LogError($"Error {nameof(GetFriendRequests)} - Error: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent("Unable to process friend requests count.")
                };
            }

            var json = JsonConvert.SerializeObject(requests);
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };
        }
    }
}


<|start_filename|>TurnipTracker/Model/Profile.cs<|end_filename|>
﻿using System;
using MvvmHelpers;
using Newtonsoft.Json;

namespace TurnipTracker.Model
{
    public class Profile : ObservableObject
    {
        [JsonIgnore]
        public Action SaveProfileAction { get; set; }

        string name = string.Empty;
        public string Name
        {
            get => name;
            set
            {
                var force = false;
                if (value.Length > 15)
                {
                    value = value.Substring(0, 15);
                    force = true;
                }
                SetProperty(ref name, value, onChanged: SaveProfileAction);
                if (force)
                    OnPropertyChanged();
            }
        }

        string friendCode = string.Empty;
        public string FriendCode
        {
            get => friendCode;
            set
            {                
                SetProperty(ref friendCode, value, onChanged: SaveProfileAction);
            }
        }

        string islandName = string.Empty;
        public string IslandName
        {
            get => islandName;
            set
            {
                var force = false;
                if (value.Length > 15)
                {
                    value = value.Substring(0, 15);
                    force = true;
                }
                SetProperty(ref islandName, value, onChanged: SaveProfileAction);
                if (force)
                    OnPropertyChanged();
            }
        }

        string status = "😍";
        public string Status
        {
            get => status;
            set
            {
                var force = false;
                if (value.Length > 4)
                {
                    value = value.Substring(0, 4);
                    force = true;
                }
                SetProperty(ref status, value, onChanged: SaveProfileAction);
                if (force)
                    OnPropertyChanged();
            }
        }

        int fruit = (int)Model.Fruit.Apple;
        public int Fruit
        {
            get => fruit;
            set => SetProperty(ref fruit, value, onChanged: SaveProfileAction);
        }

        int gateStatus = (int)Model.GateStatus.Closed;
        public int GateStatus
        {
            get => gateStatus;
            set => SetProperty(ref gateStatus, value, onChanged: SaveProfileAction);
        }

        string dodoCode = string.Empty;
        public string DodoCode
        {
            get => dodoCode;
            set => SetProperty(ref dodoCode, value, onChanged: SaveProfileAction);
        }

        double gatesOpenLength = 0.5;
        public double GatesOpenLength
        {
            get => gatesOpenLength;
            set
            {
                GateClosesAtUTC = DateTime.UtcNow.AddMinutes(60 * value);
                SetProperty(ref gatesOpenLength, value, onChanged: SaveProfileAction);
            }
        }

        public DateTime GateClosesAtUTC { get; set; } = DateTime.UtcNow;

        public string TimeZone { get; set; }
    }
}


<|start_filename|>TurnipTracker/Model/PredictionPattern.cs<|end_filename|>
﻿using System;
namespace TurnipTracker.Model
{
    public enum PredictionPattern
    {
        IDontKnow,
        Fluctuating,
        SmallSpike,
        LargeSpike,
        Decreasing,
        All,
    }
}

<|start_filename|>TurnipTracker/AssemblyInfo.cs<|end_filename|>
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
[assembly: ExportFont("CustomFont.ttf", Alias = "AnimalCrossing")]
[assembly: ExportFont("fa-regular-400.ttf", Alias = "FA-R")]
[assembly: ExportFont("fa-solid-900.ttf", Alias = "FA-S")]
[assembly: ExportFont("fa-brands-400.ttf", Alias = "FA-B")]

<|start_filename|>TurnipTracker/View/TrackingPage.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using TurnipTracker.Services;
using TurnipTracker.ViewModel;
using Xamarin.Forms;

namespace TurnipTracker.View
{
    public partial class TrackingPage : ContentPage
    {
        TrackingViewModel vm;
        public TrackingPage()
        {
            InitializeComponent();
            BindingContext = vm = new TrackingViewModel();
            Chart.SuspendSeriesNotification();
        }

        protected override bool OnBackButtonPressed()
        {
            if (vm.IsBusy)
                return false;

            return base.OnBackButtonPressed();
        }

        private void SfExpander_Expanded(object sender, Syncfusion.XForms.Expander.ExpandedAndCollapsedEventArgs e)
        {
            if (ChartExpander.IsExpanded)
                Chart.ResumeSeriesNotification();
            else
                Chart.SuspendSeriesNotification();
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();
            vm.OnAppearing();
        }
    }
}


<|start_filename|>TurnipTracker.iOS/AppDelegate.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;

using Foundation;
using StoreKit;
using Syncfusion.SfNumericTextBox.XForms.iOS;
using Syncfusion.SfNumericUpDown.XForms.iOS;
using Syncfusion.XForms.iOS.ComboBox;
using Syncfusion.XForms.iOS.MaskedEdit;
using Syncfusion.XForms.iOS.Shimmer;
using Syncfusion.XForms.iOS.TextInputLayout;
using TurnipTracker.Services;
using UIKit;

namespace TurnipTracker.iOS
{
    // The UIApplicationDelegate for the application. This class is responsible for launching the 
    // User Interface of the application, as well as listening (and optionally responding) to 
    // application events from iOS.
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            global::Xamarin.Forms.FormsMaterial.Init();

            SfMaskedEditRenderer.Init();
            new SfNumericTextBoxRenderer();
            new SfComboBoxRenderer();
            SfTextInputLayoutRenderer.Init();

            SfShimmerRenderer.Init();
            SfNumericUpDownRenderer.Init();
            new Syncfusion.SfBusyIndicator.XForms.iOS.SfBusyIndicatorRenderer();
            Syncfusion.XForms.iOS.EffectsView.SfEffectsViewRenderer.Init();
            Syncfusion.SfChart.XForms.iOS.Renderers.SfChartRenderer.Init();
            Syncfusion.XForms.iOS.Expander.SfExpanderRenderer.Init();
            Sharpnado.MaterialFrame.iOS.iOSMaterialFrameRenderer.Init();
            Syncfusion.XForms.iOS.Buttons.SfSegmentedControlRenderer.Init();
            LoadApplication(new App());

            //initialize current one.
            Plugin.InAppBilling.InAppBillingImplementation.OnShouldAddStorePayment = OnShouldAddStorePayment;
            var current = Plugin.InAppBilling.CrossInAppBilling.Current;

            return base.FinishedLaunching(app, options);
        }

        public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
        {
            Xamarin.Forms.Application.Current.SendOnAppLinkRequestReceived(new Uri(url.AbsoluteString));
            /*var myApp = App.Current as TurnipTracker.App;
            if (null != myApp && null != url)
            {
                myApp.OnAppLinkRequestReceived(new Uri(url.AbsoluteString));
            }*/
            return true;

        }

        bool OnShouldAddStorePayment(SKPaymentQueue queue, SKPayment payment, SKProduct product)
        {
            SettingsService.IsPro = true;
            SettingsService.NeedsProSync = true;
            return true;
        }


    }
}


<|start_filename|>TurnipTracker/Converters/FriendPredictionConverter.cs<|end_filename|>
﻿using System;
using System.Globalization;
using TurnipTracker.Shared;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class FriendPredictionConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return false;

            if (value is FriendStatus status)
            {
                if (status.MinPrediction == 0 || status.MinPrediction == 999 || status.MaxPrediction == 0 || status.MaxPrediction == 999)
                    return string.Empty;

                if (status.MinPrediction == status.MaxPrediction)
                    return $"🔮 {status.MinPrediction}";

                return $"🔮 {status.MinPrediction}-{status.MaxPrediction}";
            }

            return false;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Model/FriendEntity.cs<|end_filename|>
﻿using System;
using Microsoft.WindowsAzure.Storage.Table;

namespace TurnipTracker.Shared
{
    public class FriendEntity : TableEntity
    {
        public FriendEntity()
        {

        }
        public FriendEntity(string requesterPublicKey, string requesteePublicKey)
        {
            PartitionKey = requesterPublicKey;
            RowKey = requesteePublicKey;
        }

        // PartitionKey
        public string RequesterPublicKey => PartitionKey;

        // RowKey
        public string RequesteePublicKey => RowKey;
    }
}


<|start_filename|>TurnipTracker.Functions/FriendRequests/GetFriendRequests.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Web.Http;
using TurnipTracker.Shared;
using TurnipTracker.Functions.Helpers;
using System.Collections.Generic;
using System.Net.Http;
using System.Net;
using System.Text;
using System.Linq;

namespace TurnipTracker.Functions
{
    public static class GetFriendRequests
    {
        [FunctionName(nameof(GetFriendRequests))]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetFriendRequests/{myPublicKey}")] HttpRequest req,
            string myPublicKey,
            [Table("FriendRequest")] CloudTable friendRequestTable,
            [Table("Friend")] CloudTable friendTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(SubmitFriendRequest)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new HttpResponseMessage(HttpStatusCode.Unauthorized)
                {
                    Content = new StringContent("You are not authorized to make this request.")
                };

            if (string.IsNullOrWhiteSpace(myPublicKey))
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Invalid data to process request")
                };
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, myPublicKey);
                if (user == null)
                    return new HttpResponseMessage(HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Unable to locate your user account.")
                    };
            }
            catch (Exception ex)
            {
                log.LogError("User doesn't exist: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Unable to locate your user account.")
                };
            }


            var requests = new List<PendingFriendRequest>();
            try
            {
                var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", 
                    QueryComparisons.Equal, myPublicKey);

                var rangeQuery = new TableQuery<FriendRequestEntity>().Where(publicKeyFilter);

                var existingFriendRequests = await friendRequestTable.ExecuteQuerySegmentedAsync(rangeQuery, null);

                // spin off tasks for finding all friends
                var tasks = new List<Task<TableQuerySegment<UserEntity>>>();
                foreach (var f in existingFriendRequests)
                {
                    tasks.Add(Utils.FindFriendTask(userTable, f.RequesterPublicKey));
                }

                await Task.WhenAll(tasks);

                for (var i = 0; i < tasks.Count; i++)
                {
                    var t = tasks[i];
                    var friend = t.Result?.Results?.FirstOrDefault();
                    if (friend == null)
                        if (friend == null)
                        continue;
                    requests.Add(new PendingFriendRequest
                    {
                        CreationDateUTC = existingFriendRequests.ElementAt(i).Timestamp.UtcDateTime,
                        IslandName = friend.IslandName,
                        Name = friend.Name,
                        RequesterPublicKey = friend.PublicKey
                    });
                }

            }
            catch (Exception ex)
            {
                log.LogError($"Error {nameof(GetFriendRequests)} - Error: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent("Unable to process friend information.")
                };
            }

            var json = JsonConvert.SerializeObject(requests);
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };
        }
    }
}


<|start_filename|>TurnipTracker/AppShell.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using TurnipTracker.Services;
using TurnipTracker.View;
using TurnipTracker.View.Utils;
using Xamarin.Forms;

namespace TurnipTracker
{
    public partial class AppShell : Xamarin.Forms.Shell
    {
        public AppShell()
        {
            InitializeComponent();

#if DEBUG
            //SettingsService.IsPro = true;
            //SettingsService.NeedsProSync = true;
#endif

            if(!SettingsService.FirstRun)
                MainTabBar.CurrentItem = MainTabBar.Items[1];

            Routing.RegisterRoute("invite", typeof(SubmitFriendRequestPage));
            Routing.RegisterRoute("settings", typeof(SettingsPage));
            Routing.RegisterRoute("friendrequests", typeof(FriendRequestPage));
            Routing.RegisterRoute("calc-howmanybells", typeof(CalcHowManyBells));
            Routing.RegisterRoute("calc-howmanyturnips", typeof(CalcHowManyTurnips));
            Routing.RegisterRoute("profile", typeof(ProfilePage));
            Routing.RegisterRoute("about", typeof(AboutPage));
            Routing.RegisterRoute("pro", typeof(ProPage));
        }
    }
}

<|start_filename|>TurnipTracker/ViewModel/ProfileViewModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using MvvmHelpers;
using MvvmHelpers.Commands;
using TurnipTracker.Helpers;
using TurnipTracker.Model;
using TurnipTracker.Services;
using Xamarin.Essentials;

namespace TurnipTracker.ViewModel
{
    public class ProfileViewModel : ViewModelBase
    {

        public AsyncCommand<Xamarin.Forms.View> UpsertProfileCommand { get; }
        public Profile Profile { get; }

        public string SyncCreateText => SettingsService.HasRegistered ? "Sync" : "Create";
        public bool NeedsProfile => !SettingsService.HasRegistered;
        bool needsSync;
        public bool NeedsSync
        {
            get => needsSync;
            set => SetProperty(ref needsSync, value);
        }
        public ProfileViewModel()
        {
            if (Xamarin.Forms.DesignMode.IsDesignModeEnabled)
                return;

            Profile = DataService.GetProfile();
            Profile.SaveProfileAction = SaveProfile;

            UpsertProfileCommand = new AsyncCommand<Xamarin.Forms.View>(UpsertProfile);
        }

        void SaveProfile()
        {
            DataService.SaveProfile(Profile);
            NeedsSync = true;
            SettingsService.UpdateProfile = true;
        }

        async Task UpsertProfile(Xamarin.Forms.View element)
        {
            if (IsBusy)
                return;

            var firstCreate = NeedsProfile;

            if (string.IsNullOrWhiteSpace(Profile.Name))
            {
                await DisplayAlert("Update Profile", "Please enter your nickname.");
                return;
            }

            if (string.IsNullOrWhiteSpace(Profile.IslandName))
            {
                await DisplayAlert("Update Profile", "Please enter your island name.");
                return;
            }

            if (!(await CheckConnectivity("Check connectivity", "Unable to update profile, please check internet and try again")))
                return;

            Analytics.TrackEvent("SyncStatus");

            try
            {
                IsBusy = true;
                await DataService.UpsertUserProfile(Profile);

                OnPropertyChanged(nameof(NeedsProfile));
                OnPropertyChanged(nameof(SyncCreateText));
                NeedsSync = false;

                if (firstCreate)
                    await Transfer(element);
            }
            catch (Exception ex)
            {
                await DisplayAlert("Sync Error", ex.Message);
                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        async Task Transfer(Xamarin.Forms.View element)
        {
           
            var result = await DisplayAlert("Backup account", "It is recommended that you backup your account codes. This will export your credentials that you can re-import on another device in the settings page.", "OK", "Not Now");
            if (!result)
                return;

            var info = await SettingsService.TransferOut();

            var bounds = element.GetAbsoluteBounds();

            await Share.RequestAsync(new ShareTextRequest
            {
                PresentationSourceBounds = bounds.ToSystemRectangle(),
                Title = "Island Tracker for ACNH Backup Codes",
                Text = info
            });

            Analytics.TrackEvent("Transfer", new Dictionary<string, string>
            {
                ["type"] = "out"
            });
        }
    }
}


<|start_filename|>TurnipTracker/View/SettingsPage.xaml.cs<|end_filename|>
﻿using TurnipTracker.ViewModel;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Xamarin.Forms.Xaml;

namespace TurnipTracker.View
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class SettingsPage : ContentPage
    {
        SettingsViewModel vm;
        public SettingsPage()
        {
            InitializeComponent();
            BindingContext = vm = new SettingsViewModel();
        }

        protected override void OnSizeAllocated(double width, double height)
        {
            var safeInsets = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
            safeInsets.Bottom = 0;
            Padding = safeInsets;
            base.OnSizeAllocated(width, height);
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            ComboBoxAutoRefreshHours.SelectedItem = ComboBoxAutoRefreshHours.ComboBoxSource[vm.RefreshAfterHours];
        }
    }
}

<|start_filename|>TurnipTracker/Converters/FruitImageConverter.cs<|end_filename|>
﻿using System;
using System.Globalization;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class FruitImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return false;
            if (value is int fruit)
            {
                return fruit switch
                {
                    0 => "apple.png",
                    1 => "cherry.png",
                    2 => "orange.png",
                    3 => "peach.png",
                    4 => "pear.png",
                    _ => "bells.png"
                };
            }

            /*
             *  <x:String>🍎 Apple</x:String>
                <x:String>🍒 Cherry</x:String>
                <x:String>🍊 Orange</x:String>
                <x:String>🍑 Peach</x:String>
                <x:String>🍐 Pear</x:String>
             */
            return "bells.png";
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker/ViewModel/SettingsViewModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AppCenter.Analytics;
using MvvmHelpers.Commands;
using TurnipTracker.Helpers;
using TurnipTracker.Services;
using Xamarin.Essentials;

namespace TurnipTracker.ViewModel
{
    public class SettingsViewModel : ViewModelBase
    {
        public AsyncCommand<Xamarin.Forms.View> TransferCommand { get; }
        public AsyncCommand DeleteAccountCommand { get; }

        public SettingsViewModel()
        {
            TransferCommand = new AsyncCommand<Xamarin.Forms.View>(Transfer);
            DeleteAccountCommand = new AsyncCommand(DeleteAccount);

        }

        async Task Transfer(Xamarin.Forms.View element)
        {

            var choice = await App.Current.MainPage.DisplayActionSheet("Transfer profile?", "Cancel", null, "Transfer to another device", "Transfer to this device");


            if (choice.Contains("another"))
            {

                if (await DisplayAlert("Transfer profile out", "This will export your credentials that you can re-import on another device. Your credentials will remain on this device. Do you want to proceed?", "Yes, transfer", "Cancel"))
                {
                    var info = await SettingsService.TransferOut();
                    var bounds = element.GetAbsoluteBounds();

                    await Share.RequestAsync(new ShareTextRequest
                    {
                        PresentationSourceBounds = bounds.ToSystemRectangle(),
                        Title = "Island Tracker for ACNH Backup Codes",
                        Text = info
                    });

                    Analytics.TrackEvent("Transfer", new Dictionary<string, string>
                    {
                        ["type"] = "out"
                    });
                }
            }
            else if (choice.Contains("this device"))
            {
                if (await DisplayAlert("Transfer in profile?", "Warning! This will start a transfer process that will override your existing profile. Ensure that you have exported your existing profile first as you can not go back. When a new account is imported your Pro status will be reset and you will have to retrieve it again. Do you still want to proceed?", "Yes, transfer in", "Cancel"))
                {
                    var info = await App.Current.MainPage.DisplayPromptAsync("Entry transfer code", "Enter your transfer code that you exported to continue.", "OK", "Cancel");

                    if (string.IsNullOrWhiteSpace(info) || info == "Cancel")
                        return;

                    Analytics.TrackEvent("Transfer", new Dictionary<string, string>
                    {
                        ["type"] = "in"
                    });

                    if (await SettingsService.TransferIn(info))
                    {
                        await DisplayAlert("Success", "Your profile has been updated. Ensure you update information in the app and sync with the cloud.");
                        SettingsService.IsPro = false;
                        SettingsService.NeedsProSync = false;
                    }
                    else
                    {
                        await DisplayAlert("Error", "Please contact support with your transfer code for help.");
                    }
                }
            }
        }

        async Task DeleteAccount()
        {
            try
            {
                Analytics.TrackEvent("DeleteAccount");
                var info = await SettingsService.TransferOut();
                var message = new EmailMessage
                {
                    Subject = $"Delete Island Tracker Account",
                    Body = info,
                    To = new List<string> { "<EMAIL>" }
                };

                await Email.ComposeAsync(message);
            }
            catch (Exception ex)
            {
                await DisplayAlert("Unable to send email", "Email <EMAIL> directly.");
            }
        }

        public bool HideFirstTimeBuying
        {
            get => SettingsService.HideFirstTimeBuying;
            set => SettingsService.HideFirstTimeBuying = value;
        }

        public bool AutoRefreshFriends
        {
            get => SettingsService.AutoRefreshFriends;
            set
            {
                SettingsService.AutoRefreshFriends = value;
                OnPropertyChanged();
            }
        }

        public int RefreshAfterHours
        {
            get => SettingsService.RefreshAfterHours switch
            {
                1 => 0,
                2 => 1,
                4 => 2,
                6 => 3,
                12 => 4,
                24 => 5,
                _ => 0
            };
            set
            {
                var val = value switch
                {
                    0 => 1,
                    1 => 2,
                    2 => 4,
                    3 => 6,
                    4 => 12,
                    5 => 24,
                    _ => 1
                };
                SettingsService.RefreshAfterHours = val;
            }
        }
    }
}


<|start_filename|>TurnipTracker.Functions/FriendRequests/RemoveFriend.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Web.Http;
using TurnipTracker.Shared;
using TurnipTracker.Functions.Helpers;

namespace TurnipTracker.Functions
{
    public static class RemoveFriend
    {
        [FunctionName(nameof(RemoveFriend))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "RemoveFriend/{myPublicKey}/{friendPublicKey}")] HttpRequest req,
            string myPublicKey,
            string friendPublicKey,
            [Table("Friend")] CloudTable friendTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(RemoveFriend)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new UnauthorizedResult();


            if (string.IsNullOrWhiteSpace(myPublicKey) ||
                string.IsNullOrWhiteSpace(friendPublicKey))
            {
                return new BadRequestErrorMessageResult("Invalid data to process request");
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, myPublicKey);
                if(user == null)
                    return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }
            catch (Exception ex)
            {
                log.LogInformation("User doesn't exist: " + ex.Message);
                return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }


            try
            {
                var requester = myPublicKey;
                var requestee = friendPublicKey;

                
                var removeOperation1 = TableOperation.Delete(new FriendRequestEntity(requester, requestee)
                {
                    ETag = "*"
                });
                var removeOperation2 = TableOperation.Delete(new FriendRequestEntity(requestee, requester)
                {
                    ETag = "*"
                });


                // Execute the operation.
                var result = await friendTable.ExecuteAsync(removeOperation1);
                if (result == null)
                    return new InternalServerErrorResult();

                result = await friendTable.ExecuteAsync(removeOperation2);
                if (result == null)
                    return new InternalServerErrorResult();
            }
            catch (Exception ex)
            {
                log.LogInformation($"Error {nameof(RemoveFriend)} - Error: " + ex.Message);
                return new InternalServerErrorResult();
            }

            return new OkObjectResult("Friendship ended");
        }
    }
}


<|start_filename|>TurnipTracker.Functions/settings.sample.json<|end_filename|>
{
    "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "MAX_FRIENDS": "15",
    "MAX_FREE_FRIENDS": "2",
    "ENABLE_PRO": "false",
    "AUTO_PRO": "false"
  }
}

<|start_filename|>TurnipTracker/ViewModel/HomeViewModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using MvvmHelpers;
using MvvmHelpers.Commands;
using TurnipTracker.Model;
using TurnipTracker.Services;

namespace TurnipTracker.ViewModel
{
    public class HomeViewModel : ViewModelBase
    {

        public AsyncCommand FirstRunCommand { get; }
        public AsyncCommand UpsertProfileCommand { get; }
        public Profile Profile { get; set; }

        bool needsSync;
        public bool NeedsSync
        {
            get => needsSync;
            set => SetProperty(ref needsSync, value);
        }

        public HomeViewModel()
        {
            if (Xamarin.Forms.DesignMode.IsDesignModeEnabled)
                return;

            Profile = DataService.GetProfile();
            Profile.SaveProfileAction = SaveProfile;

            UpsertProfileCommand = new AsyncCommand(UpsertProfile);

            FirstRunCommand = new AsyncCommand(FirstRun);
        }


        async Task FirstRun()
        {
            if (SettingsService.FirstRun)
            {
                SettingsService.FirstRun = false;
                await DisplayAlert("Welcome!", "Welcome to Island Tracker, your social turnip tracking companion. Let's get started by creating your profile.");
                await GoToAsync("profile");
            }
            else if(SettingsService.UpdateProfile)
            {
                SettingsService.UpdateProfile = false;
                Profile = DataService.GetProfile();
                Profile.SaveProfileAction = SaveProfile;
                OnPropertyChanged(nameof(Profile));
                OnPropertyChanged(nameof(IsProAndNotBusy));
            }
            else
            {
                OnPropertyChanged(nameof(IsProAndNotBusy));
            }

        }



        void SaveProfile()
        {
            DataService.SaveProfile(Profile);
            NeedsSync = true;
        }

        async Task UpsertProfile()
        {
            if (IsBusy)
                return;

            if (!SettingsService.HasRegistered)
            {
                await DisplayAlert("Create Profile", "Please create your profile before updating your status.");
                await GoToAsync("profile");
                return;
            }

            if (string.IsNullOrWhiteSpace(Profile.Name) ||  string.IsNullOrWhiteSpace(Profile.IslandName))
            {
                await DisplayAlert("Profile needs updates", "Please ensure your profile has a nickname and island name.");
                await GoToAsync("profile");
                return;
            }

            if(Profile.GateStatus == 3 && Profile.DodoCode?.Length != 5)
            {
                await DisplayAlert("Invalid Dodo Code", "Please enter a valid Dodo code, it should be 5 characters in length.");
                
                return;
            }

            if (!(await CheckConnectivity("Check connectivity", "Unable to update profile, please check internet and try again")))
                return;

            Analytics.TrackEvent("SyncProfile");

            try
            {
                IsBusy = true;
                OnPropertyChanged(nameof(IsProAndNotBusy));
                await DataService.UpsertUserProfile(Profile);
                NeedsSync = false;
            }
            catch (Exception ex)
            {
                await DisplayAlert("Sync Error", ex.Message);
                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
                OnPropertyChanged(nameof(IsProAndNotBusy));
            }
        }
    }
}


<|start_filename|>TurnipTracker/Converters/SelectedDayColorConverter.cs<|end_filename|>
﻿using System;
using System.Globalization;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class SelectedDayColorConverter : IValueConverter
    {
        static Color textPrimary;
        static Color primary;
        static SelectedDayColorConverter()
        {
            textPrimary = (Color)App.Current.Resources["TextPrimaryColor"];
            primary = (Color)App.Current.Resources["NavigationPrimary"];
        }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool val)
                return val ? primary : textPrimary;

            return textPrimary;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Helpers/Utils.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.WindowsAzure.Storage.Table;
using TurnipTracker.Shared;

namespace TurnipTracker.Functions.Helpers
{
    public static class Utils
    {
        public static string FriendCodePassword => Environment.GetEnvironmentVariable("FRIEND_CODE_PASS");
        public static string ParseToken(HttpRequest request)
        {
            var header = AuthenticationHeaderValue.Parse(request.Headers["Authorization"]);
            var authHeader = header.Parameter;

            if (!string.IsNullOrWhiteSpace(authHeader))
            {
                var encoding = Encoding.GetEncoding("iso-8859-1");
//#if DEBUG
                //return authHeader;
//#endif
                var s = encoding.GetString(Convert.FromBase64String(authHeader));
                if (!Guid.TryParse(s, out _))
                    return null;

                return s;
            }

            return null;
        }

        public static Task<TableQuerySegment<UserEntity>> FindFriendTask(CloudTable cloudTable, string friendPublicKey)
        {
            var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, friendPublicKey);

            var rangeQuery = new TableQuery<UserEntity>().Where(publicKeyFilter);
            return cloudTable.ExecuteQuerySegmentedAsync(rangeQuery, null);
        }

        public static async Task<UserEntity> FindFriend(CloudTable cloudTable, string friendPublicKey)
        {            
            return (await FindFriendTask(cloudTable, friendPublicKey)).FirstOrDefault();
        }

        public static async Task<UserEntity> FindUserEntitySlim(CloudTable cloudTable, string privateKey, string publicKey)
        {
            var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, publicKey);
            var privateKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, privateKey);

            var rangeQuery = new TableQuery<UserEntity>()
                .Where(TableQuery.CombineFilters(publicKeyFilter, TableOperators.And, privateKeyFilter))
                .Select(new List<string> { "PartitionKey", "RowKey", "Timestamp" }); 
            return (await cloudTable.ExecuteQuerySegmentedAsync(rangeQuery, null)).FirstOrDefault();
        }

        public static async Task<UserEntity> FindUserEntity(CloudTable cloudTable, string privateKey, string publicKey)
        {
            var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, publicKey);
            var privateKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, privateKey);

            var rangeQuery = new TableQuery<UserEntity>().Where(
                TableQuery.CombineFilters(publicKeyFilter, TableOperators.And, privateKeyFilter));
            return (await cloudTable.ExecuteQuerySegmentedAsync(rangeQuery, null)).FirstOrDefault();
        }

        public static async Task<bool> ReachedMaxFriends(CloudTable couldTable, string publicKey, bool proUser)
        {
            if (Environment.GetEnvironmentVariable("JAMES_KEY") == publicKey)
                return false;

            var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, publicKey);

            var rangeQuery = new TableQuery<FriendEntity>().Where(publicKeyFilter);
            var friends = await couldTable.ExecuteQuerySegmentedAsync(rangeQuery, null);
            var count = friends.Count();

            bool.TryParse(Environment.GetEnvironmentVariable("ENABLE_PRO"), out var enablePro);

            // if pro is enabled then check the different values
            int max;
            if (enablePro)
                max = proUser ? int.Parse(Environment.GetEnvironmentVariable("MAX_FRIENDS")) : int.Parse(Environment.GetEnvironmentVariable("MAX_FREE_FRIENDS"));
            else
                max = int.Parse(Environment.GetEnvironmentVariable("MAX_FRIENDS"));

            return count >= max;
        }

        public static async Task<bool> HasFriend(CloudTable cloudTable, string requesterKey, string requesteeKey)
        {
            var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, requesterKey);
            var privateKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, requesteeKey);

            var rangeQuery = new TableQuery<FriendEntity>().Where(
                TableQuery.CombineFilters(publicKeyFilter, TableOperators.And, privateKeyFilter));
            return (await cloudTable.ExecuteQuerySegmentedAsync(rangeQuery, null)).Any();
        }

        public static async Task<UserEntity> MergeUserEntity(CloudTable cloudTable, UserEntity userEntity)
        {
            // Create the InsertOrReplace table operation
            var insertOperation = TableOperation.Merge(userEntity);

            // Execute the operation.
            var result = await cloudTable.ExecuteAsync(insertOperation);
            var mergedUser = result.Result as UserEntity;
            return mergedUser;
        }
    }
}


<|start_filename|>TurnipTracker/Model/PredictedPriceSeries.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

#nullable enable
namespace TurnipTracker.Model
{
    public sealed class PredictedPriceSeries
    {
        public string PatternDesc { get; }
        public PredictionPattern PatternNumber { get; }
        public List<(int min, int max)> Prices { get; }
        public double Probability { get; set; }

        public int WeekGuaranteedMinimum { get; set; }
        public int WeekMax { get; set; }

        public double CategoryTotalProbability { get; set; }

        public PredictedPriceSeries(string patternDesc, PredictionPattern patternNumber, List<(int min, int max)> prices, double probability = 0)
        {
            PatternDesc = patternDesc;
            PatternNumber = patternNumber;
            Prices = prices;
            Probability = probability;
        }

        internal (int min, int max) GetMinMax(int i, bool isPM)
        {
            var index = i * 2;
            if (isPM) index++;
            return Prices[index];
        }

        [Conditional("DEBUG")]
        public void Dump()
        {
            Console.Write($"{PatternDesc,20}");
            Console.Write($"{FmtPct(CategoryTotalProbability)}");
            Console.Write($"{FmtPct(Probability)}");
            for (var index = 1; index < Prices.Count; index++)
            {
                var minPrice = Prices[index].min;
                var maxPrice = Prices[index].max;
                string prices;
                if (minPrice == maxPrice)
                    prices = $"{minPrice}";
                else
                    prices = $"{minPrice}..{maxPrice}";
                Console.Write($"{prices,10}");
            }
            Console.Write($"{WeekGuaranteedMinimum,10}");
            Console.Write($"{WeekMax,10}");
            Console.WriteLine();

            static string FmtPct(double value)
            {
                if (value < 0.1)
                    return $"{value,10:0.00%}";
                return $"{value,10:0.0%}";
            }
        }

        public override string ToString() => PatternDesc;
    }
}

<|start_filename|>TurnipTracker/View/HomePage.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using TurnipTracker.Services;
using TurnipTracker.ViewModel;
using Xamarin.Forms;

namespace TurnipTracker.View
{
    public partial class HomePage : ContentPage
    {
        HomeViewModel vm;
        public HomePage()
        {
            InitializeComponent();
            BindingContext = vm = new HomeViewModel();
        }

        protected override bool OnBackButtonPressed()
        {
            if (vm.IsBusy)
                return false;

            return base.OnBackButtonPressed();
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();

            //ComboBoxGateStatus.ComboBoxSource = new List<string>
            //{
            //    "Closed",
            //    "Open to All Friends",
            //    "Open to Best Friends",
            //    "Dodo Code"
            //};

            ComboBoxGateStatus.SelectedItem = ComboBoxGateStatus.ComboBoxSource[vm.Profile.GateStatus];

            

            await vm.FirstRunCommand.ExecuteAsync();
           
        }
    }
}


<|start_filename|>TurnipTracker/View/TransactionsView.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using Xamarin.Forms;

namespace TurnipTracker.View
{
    public partial class TransactionsView : ContentView
    {
        public TransactionsView()
        {
            InitializeComponent();
        }

        async void TapGestureRecognizer_Tapped(System.Object sender, System.EventArgs e)
        {
            await App.Current.MainPage.DisplayActionSheet("Add Transaction", "Cancel", null, "Purchase", "Sale");
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Pro/GetProStatus.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Net.Http;
using System.Net;
using System.Text;
using TurnipTracker.Functions.Helpers;
using TurnipTracker.Shared;
using System.Linq;
using TurnipTracker.Functions.Model;

namespace TurnipTracker.Functions.Pro
{
    public static class GetProStatus
    {
        [FunctionName(nameof(GetProStatus))]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetProStatus/{myPublicKey}")] HttpRequest req,
            string myPublicKey,
            [Table("ProStatus")] CloudTable proTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(GetProStatus)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new HttpResponseMessage(HttpStatusCode.Unauthorized)
                {
                    Content = new StringContent("You are not authorized to make this request.")
                };

            if (string.IsNullOrWhiteSpace(myPublicKey))
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Invalid data to process request")
                };
            }



            ProStatus status = null;
            try
            {
                var publicKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, myPublicKey);
                var privateKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, privateKey);

                var rangeQuery = new TableQuery<ProStatusEntity>().Where(
                    TableQuery.CombineFilters(publicKeyFilter, TableOperators.And, privateKeyFilter));
                var entity = (await proTable.ExecuteQuerySegmentedAsync(rangeQuery, null)).FirstOrDefault();


                status = new ProStatus
                {
                    IsPro = entity != null,
                    Receipt = entity?.Receipt ?? string.Empty
                };

            }
            catch (Exception ex)
            {
                log.LogError($"Error {nameof(GetFriends)} - Error: " + ex.Message);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }

            var json = JsonConvert.SerializeObject(status);
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Model/FriendRequestEntity.cs<|end_filename|>
﻿using System;
using Microsoft.WindowsAzure.Storage.Table;

namespace TurnipTracker.Shared
{
    /// <summary>
    /// This is a friend request record
    /// It is never exposed to the client and only for internal.
    /// </summary>
    public class FriendRequestEntity : TableEntity
    {
        public FriendRequestEntity()
        {

        }
        public FriendRequestEntity(string requesterPublicKey, string requesteePublicKey)
        {
            PartitionKey = requesteePublicKey;
            RowKey = requesterPublicKey;
        }

        /// <summary>
        /// This person has a friend code and is requesting to be
        /// friends and the Requestee need to accept it
        /// </summary>
        /// PartitionKey
        public string ApproverPublicKey => PartitionKey;

        /// <summary>
        /// This is the person that shared the code, and has to do final approval
        /// </summary>
        /// RowKey
        public string RequesterPublicKey => RowKey;

    }
}


<|start_filename|>TurnipTracker/Converters/ExpiresAtConverter.cs<|end_filename|>
﻿using System;
using System.Globalization;
using TurnipTracker.Shared;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class ExpiresAtConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return false;
            if (value is DateTime time && time != null && time > DateTime.UtcNow)
            {
                var local = time.ToLocalTime();
                if (local.Day == DateTime.Now.Day)
                    return $"Closes @{local.ToShortTimeString()}";
                else
                    return $"Closes @{local.ToShortTimeString()} tmw";

            }

            return string.Empty;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker/ViewModel/FriendsViewModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AppCenter.Crashes;
using MvvmHelpers;
using MvvmHelpers.Commands;
using TurnipTracker.Services;
using TurnipTracker.Shared;
using Xamarin.Essentials;
using Xamarin.Forms;
using TurnipTracker.Helpers;
using Microsoft.AppCenter.Analytics;
using TurnipTracker.Model;

namespace TurnipTracker.ViewModel
{
    public class FriendsViewModel : ViewModelBase
    {
        public ObservableRangeCollection<FriendStatus> Friends { get; }
        public ObservableRangeCollection<FriendGroup> FriendsGrouped { get; }
        public bool ShowNoFriends => FriendsGrouped.Count == 0;
        public bool HasFriends => !ShowNoFriends;

        public AsyncCommand<string> ViewFriendCodeCommand { get; }

        public FriendsViewModel()
        {
            ViewFriendCodeCommand = new AsyncCommand<string>(ViewFriendCode);
            Friends = new ObservableRangeCollection<FriendStatus>();
            FriendsGrouped = new ObservableRangeCollection<FriendGroup>();
            RegisterFriendClipboardCommand = new AsyncCommand(RegisterFriendClipboard);
            RegisterFriendCommand = new AsyncCommand<string>(RegisterFriend);
            RefreshCommand = new AsyncCommand(RefreshAsync);
            SendFriendRequestCommand = new AsyncCommand<Xamarin.Forms.View>(SendFriendRequest);
            RemoveFriendCommand = new AsyncCommand<FriendStatus>(RemoveFriend);
            GoToFriendRequestCommand = new AsyncCommand(GoToFriendRequest);
            AddFriendManuallyCommand = new AsyncCommand(AddFriendManually);
            var cache = DataService.GetCache<IEnumerable<FriendStatus>>(DataService.FriendKey);
            if (cache != null)
            {
                Friends.ReplaceRange(cache.OrderByDescending(s => s.TurnipUpdateTimeUTC));
                UpdateFriendsGroups();
            }

            
        }

        async Task ViewFriendCode(string code)
        {
            await DisplayAlert("Friend Code", code);
            Analytics.TrackEvent("ViewFriendCode");
        }

        string requestCount = string.Empty;
        public string RequestCount
        {
            get => requestCount;
            set => SetProperty(ref requestCount, value);
        }

        public bool ForceRefresh { get; set; }

        public string LastUpdate
        {
            get
            {
                var time = SettingsService.LastFriendsUpdate;
                if (time == DateTime.MinValue)
                    return string.Empty;
                var local = time.ToLocalTime();
                return $"Last friend sync: {local.ToShortDateString()} at {local.ToShortTimeString()}";
            }
        }


        public AsyncCommand AddFriendManuallyCommand { get; set; }

        async Task AddFriendManually()
        {
            var clip = string.Empty;
            if (Clipboard.HasText)
            {
                var val = await Clipboard.GetTextAsync();
                if(val != null & val.StartsWith("acislandtracker://"))
                {
                    clip = val;
                    await Clipboard.SetTextAsync(string.Empty);
                }
            }
            var link = await App.Current.MainPage.DisplayPromptAsync("Enter friend's invite link", "Paste in your friend's invite link that they sent you.", "Submit", "Cancel", "acislandtracker://", initialValue: clip);

            if (link != null && link.StartsWith("acislandtracker://"))
            {
                try
                {
                    var uri = new Uri(link);

                    if (Clipboard.HasText)
                    {
                        var val = await Clipboard.GetTextAsync();
                        if (val != null & val.StartsWith("acislandtracker://"))
                        {
                            await Clipboard.SetTextAsync(string.Empty);
                        }
                    }

                    await Shell.Current.GoToAsync($"//{uri.Host}/{uri.PathAndQuery}");
                    Analytics.TrackEvent("AddFriendManually");
                }
                catch
                {

                }
            }
        }


        public AsyncCommand<Xamarin.Forms.View> SendFriendRequestCommand { get; set; }

        async Task SendFriendRequest(Xamarin.Forms.View element)
        {
            if(!SettingsService.HasRegistered)
            {
                await DisplayAlert("Register First", "Please create a profile before sending friend requests.");
                return;
            }

            if (SettingsService.FirstFriendRequest)
            {
                await DisplayAlert("How to send a request",
                    "Looks like this is your first time sending a friend request! Exciting! Simply share the friend invite link with your friends and have them click the link to open Island Tracker or copy it to their clipboard and go to their friends tab. Once they request you as a friend you will see the request in the 'Request' section above that you can either approve or deny.");
                SettingsService.FirstFriendRequest = false;
            }

            var name = DataService.GetProfile().Name;
            if (string.IsNullOrWhiteSpace(name))
            {
                await DisplayAlert("Update Profile", "Please update profile before sending a friend request.");
                return;
            }

            if (!(await CheckConnectivity("Check connectivity", "Unable to update profile, please check internet and try again")))
                return;

            var key = await SettingsService.GetPublicKey();
            var message = $"acislandtracker://friends/invite?id={key}&name={Uri.EscapeDataString(name)}";

            //#if DEBUG
            //            await Launcher.OpenAsync(message);
            //#else
            var bounds = element.GetAbsoluteBounds();
            await Share.RequestAsync(new ShareTextRequest
            {
                Title = "Island Tracker Invite",
                Text = message,
                PresentationSourceBounds = bounds.ToSystemRectangle()
            });
            //#endif

            Analytics.TrackEvent("SendFriendRequest");
        }

        public AsyncCommand GoToFriendRequestCommand { get; }

        async Task GoToFriendRequest()
        {
            if (!SettingsService.HasRegistered)
            {
                await App.Current.MainPage.DisplayAlert("Register First", "Create a profile before checking friend requests.", "OK");
                return;
            }

            ForceRefresh = true;

            await GoToAsync("friendrequests");
        }

        public AsyncCommand<string> RegisterFriendCommand { get; }
        public AsyncCommand RegisterFriendClipboardCommand { get; }

        public async Task<bool> RegisterFriendClipboard()
        {
            if (!Clipboard.HasText)
                return false;

            var clip = await Clipboard.GetTextAsync();
            if(await RegisterFriend(clip))
            {
                Analytics.TrackEvent("RegisterFriend", new Dictionary<string, string>
                {
                    ["type"] = "clipboard"
                });
                await Clipboard.SetTextAsync(string.Empty);
                return true;
            }

            return false;
        }

        bool parsing;
        async Task<bool> RegisterFriend(string uriString)
        {
            if (parsing)
                return false;

            parsing = true;
            try
            {
                if (string.IsNullOrWhiteSpace(uriString))
                    return false;

                var created = Uri.TryCreate(uriString, UriKind.Absolute, out var uri);

                if (!created)
                    return false;

                if (uri != null && uri.Host == "friends" && uri.Scheme == "acislandtracker")
                {

                    var key = await SettingsService.GetPublicKey();
                    if (uriString.Contains(key))
                        return false;

                    // we are already on the friends page, so no need to use the host.
                    await GoToAsync($"//{uri.Host}/{uri.PathAndQuery}", uri.Host);
                    return true;
                }
            }
            catch (Exception ex)
            {
                Crashes.TrackError(ex);
            }
            finally
            {
                parsing = false;
            }

            return false;
        }

        public AsyncCommand RefreshCommand { get; set; }
        async Task RefreshAsync()
        {
            if (IsBusy)
                return;

            if (!SettingsService.HasRegistered)
            {
                await DisplayAlert("Register First", "Please create a profile before syncing friends.");
                return;
            }

            IsBusy = true;
            var fake = Friends.Count == 0;
            try
            {
                if (fake)
                {
                    FriendsGrouped.Add(new FriendGroup("Updating...", new List<FriendStatus>
                    {
                        new FriendStatus()
                    }));
                    OnPropertyChanged(nameof(ShowNoFriends));
                }

                var friendsTask = DataService.GetFriendsAsync(ForceRefresh);
                var countTask = DataService.GetFriendRequestCountAsync(ForceRefresh);
                await Task.WhenAll(friendsTask, countTask);
                if (friendsTask.IsFaulted && friendsTask.Exception != null)
                    throw friendsTask.Exception;

                var statuses = friendsTask.Result;
                ForceRefresh = false;
                // await Task.Delay(5000);
                Friends.ReplaceRange(statuses.OrderByDescending(s => s.TurnipUpdateTimeUTC));

                UpdateFriendsGroups();

                SettingsService.LastFriendsUpdate = DateTime.UtcNow;
                OnPropertyChanged(nameof(LastUpdate));

                if (!countTask.IsFaulted)
                    RequestCount = countTask.Result.Count == 0 ? string.Empty : countTask.Result.Count.ToString();
                else
                    RequestCount = string.Empty;


                SettingsService.FriendRequestCount = RequestCount;

            }
            catch (HttpResponseException hrex) when (!string.IsNullOrWhiteSpace(hrex.Message))
            {
                if (fake)
                    FriendsGrouped.Clear();
                await DisplayAlert("Uh oh, turbulence", hrex.Message);
            }
            catch (Exception ex)
            {
                if (fake)
                    FriendsGrouped.Clear();

                await DisplayAlert("Uh oh, turbulence", "Looks like something went wrong. Check internet and try again.");

                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
            }
            OnPropertyChanged(nameof(ShowNoFriends));
            OnPropertyChanged(nameof(HasFriends));
        }

        int sortOption = 0;
        public int SortOption
        {
            get => sortOption;
            set
            {
                if (SetProperty(ref sortOption, value))
                    UpdateFriendsGroups();
            }
        }

        void UpdateFriendsGroups()
        {
            if (sortOption == 0)
            {
                var today = Friends.Where(f => f.TurnipUpdateTimeUTC.ToLocalTime().DayOfYear == DateTime.Now.DayOfYear).ToList();
                var older = Friends.Where(f => f.TurnipUpdateTimeUTC.ToLocalTime().DayOfYear != DateTime.Now.DayOfYear).ToList();

                FriendsGrouped.Clear();
                if (today.Count > 0)
                    FriendsGrouped.Add(new FriendGroup("Updated Today", today));
                if (older.Count > 0)
                    FriendsGrouped.Add(new FriendGroup("Older Updates", older));
            }
            else
            {
                var open = Friends.Where(f => f.GateStatus != 0 && f.GateClosesAtUTC.HasValue && f.GateClosesAtUTC.Value > DateTime.UtcNow).OrderBy(f => f.GateClosesAtUTC.Value).ToList();
                var closed = Friends.Where(f => f.GateStatus == 0 || !f.GateClosesAtUTC.HasValue || f.GateClosesAtUTC.Value <= DateTime.UtcNow).ToList();

                FriendsGrouped.Clear();
                if (open.Count > 0)
                    FriendsGrouped.Add(new FriendGroup("Open", open));
                if (closed.Count > 0)
                    FriendsGrouped.Add(new FriendGroup("Closed", closed));

            }
        }

        public AsyncCommand<FriendStatus> RemoveFriendCommand { get; set; }
        async Task RemoveFriend(FriendStatus friendStatus)
        {
            if (IsBusy)
                return;

            if (!(await CheckConnectivity("Check connectivity", "Unable to update profile, please check internet and try again")))
                return;

            if (!await DisplayAlert("Remove friend?", $"Are you sure you want to remove {friendStatus.Name}?", "Yes, remove", "Cancel"))
                return;

            Analytics.TrackEvent("RemoveFriend");

            IsBusy = true;
            try
            {

                await DataService.RemoveFriendAsync(friendStatus.PublicKey);
                Friends.Remove(friendStatus);
                ForceRefresh = true;
                UpdateFriendsGroups();
                OnPropertyChanged(nameof(ShowNoFriends));
                OnPropertyChanged(nameof(HasFriends));
                DataService.ClearCache(DataService.FriendKey);
            }
            catch (HttpResponseException hrex) when (!string.IsNullOrWhiteSpace(hrex.Message))
            {
                await DisplayAlert("Uh oh, turbulence", hrex.Message);
            }
            catch (Exception ex)
            {


                await DisplayAlert("Uh oh, turbulence", "Looks like something went wrong. Check internet and try again.");
                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}


<|start_filename|>TurnipTracker/Model/Day.cs<|end_filename|>
﻿using System;
using System.Linq;
using MvvmHelpers;
using Newtonsoft.Json;

namespace TurnipTracker.Model
{
    public class Day : ObservableObject
    {
        [JsonIgnore]
        public Action SaveCurrentWeekAction { get; set; }

        public string DayLong { get; set; }

        [JsonIgnore]
        public string DayShort => DayLong.FirstOrDefault().ToString();

        bool firstPurchase;
        public bool FirstPurchase
        {
            get => firstPurchase;
            set => SetProperty(ref firstPurchase, value, onChanged: SaveCurrentWeekAction);
        }

        int lastWeekPattern = (int)PredictionPattern.IDontKnow;
        public int LastWeekPattern
        {
            get => lastWeekPattern;
            set => SetProperty(ref lastWeekPattern, value, onChanged: SaveCurrentWeekAction);
        }

        int? buyPrice;

        public int? BuyPrice
        {
            get => buyPrice;
            set => SetProperty(ref buyPrice, value, onChanged: SaveCurrentWeekAction);
        }

        int? actualPurchasePrice;

        public int? ActualPurchasePrice
        {
            get => actualPurchasePrice;
            set => SetProperty(ref actualPurchasePrice, value, onChanged: SaveCurrentWeekAction);
        }

        int? priceAM;

        public int? PriceAM
        {
            get => priceAM;
            set => SetProperty(ref priceAM, value, onChanged: SaveCurrentWeekAction);
        }

        int? pricePM;
        public int? PricePM
        {
            get => pricePM;
            set => SetProperty(ref pricePM, value, onChanged: SaveCurrentWeekAction);
        }

        public bool IsSunday { get; set; }

        string differenceAM = string.Empty;
        [JsonIgnore]
        public string DifferenceAM
        {
            get => differenceAM;
            set => SetProperty(ref differenceAM, value);
        }

        string differencePM = string.Empty;
        [JsonIgnore]
        public string DifferencePM
        {
            get => differencePM;
            set => SetProperty(ref differencePM, value);
        }

        [JsonIgnore]
        public int PredictionAMMin { get; set; }
        [JsonIgnore]
        public int PredictionAMMax { get; set; }
        [JsonIgnore]
        public int PredictionPMMin { get; set; }
        [JsonIgnore]
        public int PredictionPMMax { get; set; }

        string predictionAM = string.Empty;
        [JsonIgnore]
        public string PredictionAM
        {
            get => predictionAM;
            set => SetProperty(ref predictionAM, value);
        }

        string predictionPM = string.Empty;
        [JsonIgnore]
        public string PredictionPM
        {
            get => predictionPM;
            set => SetProperty(ref predictionPM, value);
        }

        bool isSelectedDay;
        [JsonIgnore]
        public bool IsSelectedDay
        {
            get => isSelectedDay;
            set => SetProperty(ref isSelectedDay, value);
        }

        

        [JsonIgnore]
        public bool IsNotSunday => !IsSunday;
    }
}


<|start_filename|>TurnipTracker.Shared/User.cs<|end_filename|>
﻿using System;

namespace TurnipTracker.Shared
{
    /// <summary>
    /// This is information that will be pulled down for friends list
    /// </summary>
    public class User
    {
        public string Name { get; set; }
        public string IslandName { get; set; }

        //could be tiny int?
        public int Fruit { get; set; }
        public string TimeZone { get; set; }
        public string Status { get; set; }
        public string FriendCode { get; set; }
        public string PublicKey { get; set; }

        public string DodoCode { get; set; }
        public int GateStatus { get; set; }

        public DateTime? GateClosesAtUTC { get; set; }
    }

    public class TurnipUpdate
    {


        //could be small int
        public int AMPrice { get; set; }
        public int PMPrice { get; set; }

        public int MaxPrediction { get; set; }
        public int MinPrediction { get; set; }

        public int BuyPrice { get; set; }

        //could be smallint
        public int DayOfYear { get; set; }
        public int Year { get; set; }
        public string PublicKey { get; set; }

        public DateTime TurnipUpdateTimeUTC { get; set; }

    }

    public class FriendStatus
    {
        public string Name { get; set; }
        public string IslandName { get; set; }

        //could be tiny int?
        public int Fruit { get; set; }
        public string TimeZone { get; set; }
        public string Status { get; set; }
        public string PublicKey { get; set; }
        //could be small int
        public int AMPrice { get; set; }
        public int PMPrice { get; set; }
        public int MaxPrediction { get; set; }
        public int MinPrediction { get; set; }

        public int BuyPrice { get; set; }

        //could be smallint
        public int TurnipUpdateDayOfYear { get; set; }
        public int TurnipUpdateYear { get; set; }
        public DateTime TurnipUpdateTimeUTC { get; set; }
        public string FriendCode { get; set; }
        public string DodoCode { get; set; }
        public int GateStatus { get; set; }

        public DateTime? GateClosesAtUTC { get; set; }
    }
}


<|start_filename|>TurnipTracker/Converters/IsGateStatusConverter.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using TurnipTracker.Shared;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class IsGateStatusConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is int status && parameter is string test && int.TryParse(test, out var check))
                return status == check;

            return false;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class IsGateOpenConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is FriendStatus status && status.GateStatus != 0 && status.GateClosesAtUTC != null && status.GateClosesAtUTC > DateTime.UtcNow)
                return true;

            return false;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker/Model/Predictor.cs<|end_filename|>
﻿/*
Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace TurnipTracker.Model
{
    // Originally developed by <NAME>, with great thanks to Ninji
    // Original project location: https://github.com/mikebryant/ac-nh-turnip-prices
    // This is a C# version of https://github.com/mikebryant/ac-nh-turnip-prices/blob/master/js/predictions.js

    [SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Explicit types are useful when converting from dynamically typed JS")]
    public static class Predictor
    {
        // The reverse-engineered code is not perfectly accurate, especially as it's not
        // 32-bit ARM floating point. So, be tolerant of slightly unexpected inputs
        const double fudgeFactor = 5;

        static readonly Dictionary<PredictionPattern, int> patternCounts = new Dictionary<PredictionPattern, int>
        {
            { PredictionPattern.Fluctuating, 56 },
            { PredictionPattern.LargeSpike, 7 },
            { PredictionPattern.Decreasing, 1 },
            { PredictionPattern.SmallSpike, 8 },
        };

        static readonly Dictionary<PredictionPattern, Dictionary<PredictionPattern, double>> probabilityMatrix =
            new Dictionary<PredictionPattern, Dictionary<PredictionPattern, double>>
        {
            { PredictionPattern.Fluctuating ,  new Dictionary<PredictionPattern, double>
                {
                    { PredictionPattern.Fluctuating, 0.20 },
                    { PredictionPattern.LargeSpike,  0.30 },
                    { PredictionPattern.Decreasing,  0.15 },
                    { PredictionPattern.SmallSpike, 0.35 },
                }
            },
            { PredictionPattern.LargeSpike ,  new Dictionary<PredictionPattern, double>
                {
                    { PredictionPattern.Fluctuating, 0.50 },
                    { PredictionPattern.LargeSpike,  0.05 },
                    { PredictionPattern.Decreasing,  0.20 },
                    { PredictionPattern.SmallSpike, 0.25 },
                }
            },
            { PredictionPattern.Decreasing ,  new Dictionary<PredictionPattern, double>
                {
                    { PredictionPattern.Fluctuating, 0.25 },
                    { PredictionPattern.LargeSpike,  0.45 },
                    { PredictionPattern.Decreasing,  0.05 },
                    { PredictionPattern.SmallSpike, 0.25 },
                }
            },
            { PredictionPattern.SmallSpike ,  new Dictionary<PredictionPattern, double>
                {
                    { PredictionPattern.Fluctuating, 0.45 },
                    { PredictionPattern.LargeSpike,  0.25 },
                    { PredictionPattern.Decreasing,  0.15 },
                    { PredictionPattern.SmallSpike, 0.15 },
                }
            },
        };

        const double rateMultiplier = 10000;

        static int IntCeil(double val) => (int)Math.Truncate(val + 0.99999);

        static double MinimumRateFromGivenAndBase(int givenPrice, int buyPrice) => rateMultiplier * (givenPrice - 0.99999) / buyPrice;

        static double MaximumRateFromGivenAndBase(int givenPrice, int buyPrice) => rateMultiplier * (givenPrice + 0.00001) / buyPrice;

        static int GetPrice(double rate, int basePrice) => IntCeil(rate * basePrice / rateMultiplier);

        // This corresponds to the code:
        //   for (int i = start; i<start + length; i++)
        //   {
        //     sellPrices[work++] =
        //       intceil(randfloat(rateMin / RATE_MULTIPLIER, rateMax / RATE_MULTIPLIER) * basePrice);
        //   }
        //
        // Would modify the predictedPrices array.
        // If the givenPrices won't match, returns false, otherwise returns true
        static bool GenerateIndividualRandomPrice(
          int[] givenPrices, List<(int min, int max)> predictedPrices, int start, int length, double rateMin, double rateMax)
        {
            rateMin *= rateMultiplier;
            rateMax *= rateMultiplier;

            int buyPrice = givenPrices[0];

            for (int i = start; i < start + length; i++)
            {
                int minPred = GetPrice(rateMin, buyPrice);
                int maxPred = GetPrice(rateMax, buyPrice);

                if (i < givenPrices.Length && givenPrices[i] > 0)
                {
                    if (givenPrices[i] < minPred - fudgeFactor || givenPrices[i] > maxPred + fudgeFactor)
                    {
                        // Given price is out of predicted range, so this is the wrong pattern
                        return false;
                    }
                    minPred = givenPrices[i];
                    maxPred = givenPrices[i];
                }

                predictedPrices.Add((minPred, maxPred));
            }
            return true;
        }

        // This corresponds to the code:
        //   rate = randfloat(start_rateMin, start_rateMax);
        //   for (int i = start; i<start + length; i++)
        //   {
        //     sellPrices[work++] = intceil(rate* basePrice);
        //     rate -= randfloat(rateDecayMin, rateDecayMax);
        //   }
        //
        // Would modify the predictedPrices array.
        // If the givenPrices won't match, returns false, otherwise returns true
        static bool GenerateDecreasingRandomPrice(
          int[] givenPrices, List<(int min, int max)> predictedPrices, int start, int length, double rateMin,
          double rateMax, double rateDecayMin, double rateDecayMax)
        {
            rateMin *= rateMultiplier;
            rateMax *= rateMultiplier;
            rateDecayMin *= rateMultiplier;
            rateDecayMax *= rateMultiplier;

            int buyPrice = givenPrices[0];

            for (int i = start; i < start + length; i++)
            {
                int minPred = GetPrice(rateMin, buyPrice);
                int maxPred = GetPrice(rateMax, buyPrice);
                if (i < givenPrices.Length && givenPrices[i] > 0)
                {
                    if (givenPrices[i] < minPred - fudgeFactor || givenPrices[i] > maxPred + fudgeFactor)
                    {
                        // Given price is out of predicted range, so this is the wrong pattern
                        return false;
                    }
                    if (givenPrices[i] >= minPred || givenPrices[i] <= maxPred)
                    {
                        // The value in the FUDGE_FACTOR range is ignored so the rate range would not be empty.
                        double realRateMin = MinimumRateFromGivenAndBase(givenPrices[i], buyPrice);
                        double realRateMax = MaximumRateFromGivenAndBase(givenPrices[i], buyPrice);
                        rateMin = Math.Max(rateMin, realRateMin);
                        rateMax = Math.Min(rateMax, realRateMax);
                    }
                    minPred = givenPrices[i];
                    maxPred = givenPrices[i];
                }

                predictedPrices.Add((minPred, maxPred));

                rateMin -= rateDecayMax;
                rateMax -= rateDecayMin;
            }
            return true;
        }

        // This corresponds to the code:
        //   rate = randfloat(rateMin, rateMax);
        //   sellPrices[work++] = intceil(randfloat(rateMin, rate) * basePrice) - 1;
        //   sellPrices[work++] = intceil(rate* basePrice);
        //   sellPrices[work++] = intceil(randfloat(rateMin, rate) * basePrice) - 1;
        //
        // Would modify the predictedPrices array.
        // If the givenPrices won't match, returns false, otherwise returns true
        static bool GeneratePeakPrice(
            int[] givenPrices, List<(int min, int max)> predictedPrices, int start, double rateMin, double rateMax)
        {
            rateMin *= rateMultiplier;
            rateMax *= rateMultiplier;

            int buyPrice = givenPrices[0];

            // Main spike 1
            int minPred = GetPrice(rateMin, buyPrice) - 1;
            int maxPred = GetPrice(rateMax, buyPrice) - 1;
            if (start < givenPrices.Length && givenPrices[start] > 0)
            {
                if (givenPrices[start] < minPred - fudgeFactor || givenPrices[start] > maxPred + fudgeFactor)
                {
                    // Given price is out of predicted range, so this is the wrong pattern
                    return false;
                }
                minPred = givenPrices[start];
                maxPred = givenPrices[start];
            }
            predictedPrices.Add((minPred, maxPred));

            // Main spike 2
            minPred = predictedPrices[start].min;
            maxPred = IntCeil(2.0 * buyPrice);
            if (start + 1 < givenPrices.Length && givenPrices[start + 1] > 0)
            {
                if (givenPrices[start + 1] < minPred - fudgeFactor || givenPrices[start + 1] > maxPred + fudgeFactor)
                {
                    // Given price is out of predicted range, so this is the wrong pattern
                    return false;
                }
                minPred = givenPrices[start + 1];
                maxPred = givenPrices[start + 1];
            }
            predictedPrices.Add((minPred, maxPred));

            // Main spike 3
            minPred = IntCeil(1.4 * buyPrice) - 1;
            maxPred = predictedPrices[start + 1].max - 1;
            if (start + 2 < givenPrices.Length && givenPrices[start + 2] > 0)
            {
                if (givenPrices[start + 2] < minPred - fudgeFactor || givenPrices[start + 2] > maxPred + fudgeFactor)
                {
                    // Given price is out of predicted range, so this is the wrong pattern
                    return false;
                }
                minPred = givenPrices[start + 2];
                maxPred = givenPrices[start + 2];
            }
            predictedPrices.Add((minPred, maxPred));

            return true;
        }

        static PredictedPriceSeries? GeneratePatternFluctuatingWithLengths(
                    int[] givenPrices,
                    int highPhase1Len,
                    int decPhase1Len,
                    int highPhase2Len,
                    int decPhase2Len,
                    int highPhase3Len)
        {
            int buyPrice = givenPrices[0];
            var predictedPrices = new List<(int min, int max)> { (buyPrice, buyPrice), (buyPrice, buyPrice) };

            // High Phase 1
            if (!GenerateIndividualRandomPrice(
                    givenPrices, predictedPrices, 2, highPhase1Len, 0.9, 1.4))
            {
                return null;
            }

            // Dec Phase 1
            if (!GenerateDecreasingRandomPrice(
                    givenPrices, predictedPrices, 2 + highPhase1Len, decPhase1Len,
                    0.6, 0.8, 0.04, 0.1))
            {
                return null;
            }

            // High Phase 2
            if (!GenerateIndividualRandomPrice(givenPrices, predictedPrices,
                2 + highPhase1Len + decPhase1Len, highPhase2Len, 0.9, 1.4))
            {
                return null;
            }

            // Dec Phase 2
            if (!GenerateDecreasingRandomPrice(
                    givenPrices, predictedPrices,
                    2 + highPhase1Len + decPhase1Len + highPhase2Len,
                    decPhase2Len, 0.6, 0.8, 0.04, 0.1))
            {
                return null;
            }

            // High Phase 3
            if (2 + highPhase1Len + decPhase1Len + highPhase2Len + decPhase2Len + highPhase3Len != 14)
            {
                throw new ApplicationException("Phase lengths don't add up");
            }

            int prevLength = 2 + highPhase1Len + decPhase1Len + highPhase2Len + decPhase2Len;
            if (!GenerateIndividualRandomPrice(
                     givenPrices, predictedPrices, prevLength, 14 - prevLength, 0.9,
                    1.4))
            {
                return null;
            }

            return new PredictedPriceSeries("Fluctuating", PredictionPattern.Fluctuating, predictedPrices);
        }

        static IEnumerable<PredictedPriceSeries> GeneratePatternFluctuating(int[] givenPrices)
        {
            for (var decPhase1Len = 2; decPhase1Len < 4; decPhase1Len++)
            {
                for (var highPhase1Len = 0; highPhase1Len < 7; highPhase1Len++)
                {
                    for (var highPhase3Len = 0; highPhase3Len < (7 - highPhase1Len - 1 + 1); highPhase3Len++)
                    {
                        var series = GeneratePatternFluctuatingWithLengths(givenPrices, highPhase1Len, decPhase1Len, 7 - highPhase1Len - highPhase3Len, 5 - decPhase1Len, highPhase3Len);
                        if (series != null)
                            yield return series;
                    }
                }
            }
        }

        static PredictedPriceSeries? GeneratePatternLargeSpikeWithPeak(
           int[] givenPrices,
           int peakStart)
        {

            int buyPrice = givenPrices[0];
            var predictedPrices = new List<(int min, int max)> { (buyPrice, buyPrice), (buyPrice, buyPrice) };

            if (!GenerateDecreasingRandomPrice(
                    givenPrices, predictedPrices, 2, peakStart - 2, 0.85, 0.9, 0.03, 0.05))
            {
                return null;
            }

            // Now each day is independent of next
            var minRandoms = new double[] { 0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4 };
            var maxRandoms = new double[] { 1.4, 2.0, 6.0, 2.0, 1.4, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9 };
            for (int i = peakStart; i < 14; i++)
            {
                if (!GenerateIndividualRandomPrice(
                        givenPrices, predictedPrices, i, 1, minRandoms[i - peakStart], maxRandoms[i - peakStart]))
                {
                    return null;
                }
            }
            return new PredictedPriceSeries("Large spike", PredictionPattern.LargeSpike, predictedPrices);
        }

        static IEnumerable<PredictedPriceSeries> GeneratePatternLargeSpike(int[] givenPrices)
        {
            for (var peakStart = 3; peakStart < 10; peakStart++)
            {
                var series = GeneratePatternLargeSpikeWithPeak(givenPrices, peakStart);
                if (series != null)
                    yield return series;
            }
        }

        static IEnumerable<PredictedPriceSeries> GeneratePatternDecreasing(int[] givenPrices)
        {
            var buyPrice = givenPrices[0];
            var predictedPrices = new List<(int min, int max)> { (buyPrice, buyPrice), (buyPrice, buyPrice) };

            if (!GenerateDecreasingRandomPrice(
                    givenPrices, predictedPrices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05))
            {
                yield break;
            }

            yield return new PredictedPriceSeries("Decreasingg", PredictionPattern.Decreasing, predictedPrices);
        }

        static PredictedPriceSeries? GeneratePatternSmallSpikeWithPeak(
            int[] givenPrices,
            int peakStart)
        {
            var buyPrice = givenPrices[0];
            var predictedPrices = new List<(int min, int max)> { (buyPrice, buyPrice), (buyPrice, buyPrice) };

            if (!GenerateDecreasingRandomPrice(
                    givenPrices, predictedPrices, 2, peakStart - 2, 0.4, 0.9, 0.03,
                    0.05))
            {
                return null;
            }

            // The peak
            if (!GenerateIndividualRandomPrice(
                    givenPrices, predictedPrices, peakStart, 2, 0.9, 1.4))
            {
                return null;
            }

            if (!GeneratePeakPrice(
                    givenPrices, predictedPrices, peakStart + 2, 1.4, 2.0))
            {
                return null;
            }

            if (peakStart + 5 < 14)
            {
                if (!GenerateDecreasingRandomPrice(
                        givenPrices, predictedPrices, peakStart + 5,
                        14 - (peakStart + 5), 0.4, 0.9, 0.03, 0.05))
                {
                    return null;
                }
            }

            return new PredictedPriceSeries("Small spike", PredictionPattern.SmallSpike, predictedPrices);
        }

        static IEnumerable<PredictedPriceSeries> GeneratePatternSmallSpike(int[] givenPrices)
        {
            for (var peakStart = 2; peakStart < 10; peakStart++)
            {
                var series = GeneratePatternSmallSpikeWithPeak(givenPrices, peakStart);
                if (series != null)
                    yield return series;
            }
        }

        static List<PredictedPriceSeries> GeneratePossibilities(int[] sellPrices)
        {
            var possibilities = new List<PredictedPriceSeries>();
            if (sellPrices[0] > 0)
            {
                possibilities.AddRange(Enumerate(sellPrices));
            }
            else
            {
                for (var buyPrice = 90; buyPrice <= 110; buyPrice++)
                {
                    // Create a copy so as not to mutate passed in values
                    var iterSellPrices = new int[sellPrices.Length];
                    Array.Copy(sellPrices, iterSellPrices, sellPrices.Length);
                    iterSellPrices[0] = iterSellPrices[1] = buyPrice;
                    possibilities.AddRange(Enumerate(iterSellPrices));
                }
            }
            static IEnumerable<PredictedPriceSeries> Enumerate(int[] sellPrices)
            {
                foreach (var series in GeneratePatternFluctuating(sellPrices))
                    yield return series;
                foreach (var series in GeneratePatternLargeSpike(sellPrices))
                    yield return series;
                foreach (var series in GeneratePatternDecreasing(sellPrices))
                    yield return series;
                foreach (var series in GeneratePatternSmallSpike(sellPrices))
                    yield return series;
            }
            return possibilities;
        }

        static double RowProbability(PredictedPriceSeries possibility, PredictionPattern previousPattern)
            => probabilityMatrix[previousPattern][possibility.PatternNumber] / patternCounts[possibility.PatternNumber];

        static List<PredictedPriceSeries> GetProbabilities(List<PredictedPriceSeries> possibilities, PredictionPattern previousPattern)
        {
            if (previousPattern == PredictionPattern.IDontKnow || previousPattern == PredictionPattern.All)
            {
                return possibilities;
            }

            var maxPercent = possibilities.Select((poss) => RowProbability(poss, previousPattern)).Sum();

            for (int i = 0; i < possibilities.Count; i++)
            {
                possibilities[i].Probability = RowProbability(possibilities[i], previousPattern) / maxPercent;
            }
            return possibilities;
        }

        public static List<PredictedPriceSeries> AnalyzePossibilities(int[] sellPrices, PredictionPattern previousPattern)
        {
            if (sellPrices == null) throw new ArgumentNullException(nameof(sellPrices));

            var generatedPossibilities = GeneratePossibilities(sellPrices);
            GetProbabilities(generatedPossibilities, previousPattern);

            var dailyMinMax = GetDailyMinMax(generatedPossibilities);
            generatedPossibilities.Add(dailyMinMax);

            return generatedPossibilities;
        }

        public static PredictedPriceSeries GetDailyMinMax(int[] sellPrices)
        {
            if (sellPrices == null) throw new ArgumentNullException(nameof(sellPrices));

            var generatedPossibilities = GeneratePossibilities(sellPrices);
            var dailyMinMax = GetDailyMinMax(generatedPossibilities);
            return dailyMinMax;
        }

        static PredictedPriceSeries GetDailyMinMax(List<PredictedPriceSeries> series)
        {
            var weekMinMaxs = new List<(int min, int max)>();
            for (var i = 0; i < 14; i++)
            {
                int min;
                int max;
                if (series.Count == 0)
                {
                    // If no series found then return 0 to 999
                    // This happens when the input data is invalid
                    min = 0;
                    max = 999;
                }
                else
                {
                    min = int.MaxValue;
                    max = 0;
                    foreach (var entry in series)
                    {
                        if (entry.Prices[i].min < min)
                            min = entry.Prices[i].min;
                        if (entry.Prices[i].max > max)
                            max = entry.Prices[i].max;
                    }
                }
                weekMinMaxs.Add((min, max));
            }
            return new PredictedPriceSeries("All patterns", PredictionPattern.All, weekMinMaxs);
        }
    }
}


<|start_filename|>TurnipTracker/Model/ChartDataModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Model
{
    public class ChartDataModel
    {
        public string Name { get; set; }
        public string Day { get; set; }

        public double Low { get; set; }

        public double High { get; set; }
        public double PurchasePrice { get; set; }

        public bool Purchased { get; set; }
        public bool Prediction { get; set; }

        public ChartDataModel(string name, string day, double high, double low)
        {
            Name = name;
            Day = day;
            Low = low;
            High = high;
            Prediction = true;
        }

        public ChartDataModel(string name, string day, double purchasePrice)
        {
            Name = name;
            Day = day;
            PurchasePrice = Low = High = purchasePrice;
            Purchased = true;
        }
    }
}


<|start_filename|>TurnipTracker/View/FriendsPage.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using TurnipTracker.Services;
using TurnipTracker.ViewModel;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace TurnipTracker.View
{
    public partial class FriendsPage : ContentPage
    {
        FriendsViewModel vm;
        public FriendsPage()
        {
            InitializeComponent();
            BindingContext = vm = new FriendsViewModel();
        }

        protected override async void OnAppearing()
        {


            vm.RequestCount = SettingsService.FriendRequestCount;


            if(!App.ReceivedAppLink && !(await vm.RegisterFriendClipboard()))
            {
#if !DEBUG
                // if older than 1 hour then refresh
                if (SettingsService.HasRegistered && SettingsService.AutoRefreshFriends && SettingsService.LastFriendsUpdate < DateTime.UtcNow.AddHours(-SettingsService.RefreshAfterHours))
                    _ = vm.RefreshCommand.ExecuteAsync().ContinueWith((r) => { });
                else if(SettingsService.ForceRefreshFriends)
                {
                    SettingsService.ForceRefreshFriends = false;
                    vm.ForceRefresh = true;
                    _ = vm.RefreshCommand.ExecuteAsync().ContinueWith((r) => { });
                }
#endif         
            }

            App.ReceivedAppLink = false;


            base.OnAppearing();
        }


    }
}


<|start_filename|>UserSettings/UserSettings/Form1.cs<|end_filename|>
using System.Text;

namespace UserSettings
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void ButtonEncode_Click(object sender, EventArgs e)
        {
            try
            {

                var encoding = Encoding.GetEncoding("iso-8859-1");
                var authenticationBytes = encoding.GetBytes($"{TextBoxPrivate.Text.Trim()}|{TextBoxPublic.Text.Trim()}");
                TextBoxEncoded.Text = Convert.ToBase64String(authenticationBytes);
            }
            catch (Exception)
            {

            }
        }
        private void ButtonDecode_Click(object sender, EventArgs e)
        {
            try
            {
                var encoding = Encoding.GetEncoding("iso-8859-1");
                var s = encoding.GetString(Convert.FromBase64String(TextBoxExported.Text));

                var keys = s.Split('|');
                if (keys.Length != 2)
                    return;

                TextBoxDecodedPrivate.Text = keys[0];
                TextBoxDecodedPublic.Text = keys[1];

            }
            catch (Exception)
            {
            }        
        }
    }
}

<|start_filename|>TurnipTracker.Shared/PendingFriendRequest.cs<|end_filename|>
﻿using System;
namespace TurnipTracker.Shared
{
    /// <summary>
    /// This is a friend request that will be visible to the
    /// person that needs to accept it, not in the database at all
    /// </summary>
    public class PendingFriendRequest
    {
        public string Name { get; set; }
        public string IslandName { get; set; }
        public string RequesterPublicKey { get; set; }

        public DateTime CreationDateUTC { get; set; }
    }
}


<|start_filename|>TurnipTracker.Shared/ProStatus.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Shared
{
    public class ProStatus
    {
        public string PublicKey { get; set; }
        public bool IsPro { get; set; }

        public string Receipt { get;set; }
    }
}


<|start_filename|>TurnipTracker.iOS/Renderers/BorderlessEntryRenderer.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Foundation;
using TurnipTracker.Controls;
using TurnipTracker.iOS.Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(BorderlessEntry), typeof(BorderlessEntryRenderer))]

namespace TurnipTracker.iOS.Renderers
{
    public class BorderlessEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.BorderStyle = UITextBorderStyle.None;
            }
        }
    }
}

<|start_filename|>TurnipTracker/Model/AttributionItem.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Model
{
    public class AttributionItem
    {
        public string Text { get; set; }
        public string Tag { get; set; }
    }
}


<|start_filename|>TurnipTracker/ViewModel/ProViewModel.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using MvvmHelpers.Commands;
using Plugin.InAppBilling;
using TurnipTracker.Services;

namespace TurnipTracker.ViewModel
{
    public class ProViewModel : ViewModelBase
    {
        public ProViewModel()
        {

        }

        string busyTitle = "Verifying Pro...";
        public string BusyTitle
        {
            get => busyTitle;
            set => SetProperty(ref busyTitle, value);
        }

        public string ProPrice => SettingsService.ProPrice;

        public AsyncCommand GetPriceCommand =>
            new AsyncCommand(GetPrice);

        async Task GetPrice()
        {
            if (IsBusy)
                return;

            if (IsPro)
                return;


            if(string.IsNullOrWhiteSpace(SettingsService.ProPrice) || SettingsService.ProPriceDate.AddDays(2) < DateTime.UtcNow)
            {

            }
            else
            {
                return;
            }



            BusyTitle = "Syncing prices...";

            IsBusy = true;

            try
            {

#if DEBUG
                SettingsService.ProPrice = "$2.99";
                OnPropertyChanged(nameof(ProPrice));
                return;
#endif



                //Check Offline

                var connected = await CrossInAppBilling.Current.ConnectAsync();

                if (!connected)
                {
                    return;
                }

                var items = await CrossInAppBilling.Current.GetProductInfoAsync(ItemType.InAppPurchase, productId);

                var item = items.FirstOrDefault(i => i.ProductId == productId);
                if(item != null)
                {
                    SettingsService.ProPrice = item.LocalizedPrice;
                    OnPropertyChanged(nameof(ProPrice));

                    SettingsService.ProPriceDate = DateTime.UtcNow;
                }
            }
            catch(Exception ex)
            {
                //it is alright that we couldn't get the price
            }
            finally
            {
                await CrossInAppBilling.Current.DisconnectAsync();
                IsBusy = false;
            }
        }

        public bool NeedsProSync => IsPro && SettingsService.NeedsProSync;

        public AsyncCommand PurchaseProCommand =>
            new AsyncCommand(PurchasePro);
        async Task PurchasePro()
        {
            if (IsBusy)
                return;

            if (!await CheckConnectivity("Offline", "You seem to be offline, check your internet connectivity and try again."))
                return;

            BusyTitle = "";
            Analytics.TrackEvent("ProPurchase-Start");
            IsBusy = true;


            try
            {

                //Check Offline

                var connected = await CrossInAppBilling.Current.ConnectAsync();

                if (!connected)
                {
                    await DisplayAlert("Unable to Connect", "Unable to connect to the app store, check your internet connectivity and try again.");
                    return;
                }

                Analytics.TrackEvent("ProPurchase-Connected");

                //check purchases
                var purchase = await CrossInAppBilling.Current.PurchaseAsync(productId, ItemType.InAppPurchase);

                if (purchase == null)
                {
                    Analytics.TrackEvent("ProPurchase-PurchaseComplete", new Dictionary<string, string>
                    {
                        ["purchase"] = "failed: null"
                    });
                    return;
                }
                else if (purchase.State == PurchaseState.Purchased)
                {
                    Analytics.TrackEvent("ProPurchase-PurchaseComplete", new Dictionary<string, string>
                    {
                        ["purchase"] = "success"
                    });

                    SettingsService.ProReceipt = purchase.PurchaseToken ?? string.Empty;

                    SettingsService.IsPro = true;

                    SettingsService.NeedsProSync = true;

                    try
                    {
                        await CrossInAppBilling.Current.AcknowledgePurchaseAsync(purchase.PurchaseToken);
                    }
                    catch(Exception ex)
                    {

                    }

                    try
                    {
                        await DataService.CreateProStatus(new Shared.ProStatus
                        {
                            IsPro = true,
                            Receipt = purchase.PurchaseToken ?? string.Empty
                        });
                        SettingsService.NeedsProSync = false;
                    }
                    catch (Exception ex)
                    {
                        await DisplayAlert("Pro status sync", "Purchase successful, but it looks like something went wrong syncing with server, please try again.");

                        Crashes.TrackError(ex);
                    }
                    finally
                    {
                        OnPropertyChanged(nameof(NeedsProSync));
                        OnPropertyChanged(nameof(IsPro));
                        OnPropertyChanged(nameof(IsNotPro));
                    }
                    return;
                }

                throw new InAppBillingPurchaseException(PurchaseError.GeneralError);

            }
            catch (InAppBillingPurchaseException purchaseEx)
            {
                Analytics.TrackEvent("ProPurchase-PurchaseComplete", new Dictionary<string, string>
                {
                    ["purchase"] = $"failed: {purchaseEx.PurchaseError}"
                });
                var message = string.Empty;
                switch (purchaseEx.PurchaseError)
                {
                    case PurchaseError.AppStoreUnavailable:
                        message = "Currently the app store seems to be unavailble. Try again later.";
                        break;
                    case PurchaseError.BillingUnavailable:
                        message = "Billing seems to be unavailable, please try again later.";
                        break;
                    case PurchaseError.PaymentInvalid:
                        message = "Payment seems to be invalid, please try again.";
                        break;
                    case PurchaseError.PaymentNotAllowed:
                        message = "Payment does not seem to be enabled/allowed, please try again.";
                        break;
                    case PurchaseError.UserCancelled:
                        //don't do anything
                        break;
                    default:
                        message = "Something has gone wrong, please try again.";
                        break;
                }

                if (string.IsNullOrWhiteSpace(message))
                    return;

                Console.WriteLine("Issue connecting: " + purchaseEx);
                await DisplayAlert("Uh Oh!", message);
            }
            catch (Exception ex)
            {
                Analytics.TrackEvent("ProPurchase-PurchaseComplete", new Dictionary<string, string>
                {
                    ["purchase"] = "failed: misc"
                });
                Console.WriteLine("Issue connecting: " + ex);
                await DisplayAlert("Uh Oh!", $"Looks like something has gone wrong, please try again. Code: {ex.Message}");
            }
            finally
            {
                await CrossInAppBilling.Current.DisconnectAsync();
                IsBusy = false;

                Analytics.TrackEvent("ProPurchase-Finish", new Dictionary<string, string>
                {
                    ["purchase"] = "failed: null"
                });
            }
        }

        public AsyncCommand RestorePurchasesCommand =>
            new AsyncCommand(RestorePurchases);

        const string productId = "islandtrackerpro";

        async Task RestorePurchases()
        {
            if (IsBusy)
                return;


            if (!await CheckConnectivity("Offline", "You seem to be offline, check your internet connectivity and try again."))
                return;

            BusyTitle = "Verifying purchase...";
            IsBusy = true;

            try
            {


                //Check Offline


                var connected = await CrossInAppBilling.Current.ConnectAsync();

                if (!connected)
                {
                    await DisplayAlert("Unable to Connect","Unable to connect to the app store, check your internet connectivity and try again.");
                    return;
                }

                //check purchases

                var purchases = await CrossInAppBilling.Current.GetPurchasesAsync(ItemType.InAppPurchase);

                if (purchases?.Any(p => p.ProductId == productId) ?? false)
                {
                    Analytics.TrackEvent("ProPurchase-Restore", new Dictionary<string, string>
                    {
                        ["purchase"] = "success"
                    });
                    //Purchase restored
                    SettingsService.IsPro = true;
                    SettingsService.NeedsProSync = true;
                    OnPropertyChanged(nameof(NeedsProSync));
                    OnPropertyChanged(nameof(IsPro));
                    OnPropertyChanged(nameof(IsNotPro));

                    var purchase = purchases.FirstOrDefault(p => p.ProductId == productId);

                    if(string.IsNullOrWhiteSpace(SettingsService.ProReceipt))
                    {

                        SettingsService.ProReceipt = purchase?.PurchaseToken ?? string.Empty;
                    }

                    var ack = purchase?.IsAcknowledged ?? true;
                    if(!ack)
                    {
                        try
                        {
                            await CrossInAppBilling.Current.AcknowledgePurchaseAsync(purchase.PurchaseToken);
                        }
                        catch (Exception ex)
                        {

                        }
                    }

                }
                else
                {
                    Analytics.TrackEvent("ProPurchase-Restore", new Dictionary<string, string>
                    {
                        ["purchase"] = "none found"
                    });
                    await DisplayAlert("Hmmmm!", $"Looks like we couldn't find your previous purchase. Tap on the purchase button to attempt to purchase or restore Island Tracker Pro.");
                }

            }
            catch (Exception ex)
            {
                Analytics.TrackEvent("ProPurchase-Restore", new Dictionary<string, string>
                {
                    ["purchase"] = "failed: error"
                });
                Debug.WriteLine("Issue connecting: " + ex);
                await DisplayAlert("Uh Oh!", $"Looks like something has gone wrong, please try again or tap on the Purchase button to attempt to restore this specific purchase.  Code: {ex.Message}");
            }
            finally
            {
                await CrossInAppBilling.Current.DisconnectAsync();
                IsBusy = false;
            }
        }


        public AsyncCommand SyncProStatusCommand =>
            new AsyncCommand(SyncProStatus);

        async Task SyncProStatus()
        {

            if (!await CheckConnectivity("Offline", "You seem to be offline, check your internet connectivity and try again."))
                return;


            if (IsBusy)
                return;


            BusyTitle = "Syncing status...";
            IsBusy = true;

            try
            {
                await DataService.CreateProStatus(new Shared.ProStatus
                {
                    IsPro = true,
                    Receipt = SettingsService.ProReceipt ?? string.Empty
                });
                SettingsService.NeedsProSync = false;
            }
            catch (Exception ex)
            {
                await DisplayAlert("Pro status sync", "It looks like something went wrong syncing with server, please try again.");

                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
                OnPropertyChanged(nameof(NeedsProSync));
                OnPropertyChanged(nameof(IsPro));
                OnPropertyChanged(nameof(IsNotPro));
            }
        }

        public AsyncCommand RetrieveProStatusCommand =>
            new AsyncCommand(RetrieveProStatus);


        async Task RetrieveProStatus()
        {
            if (IsBusy)
                return;

            BusyTitle = "Verifying purchase...";
            IsBusy = true;
            try
            {
                Analytics.TrackEvent("RetrieveProStatus");
                var status = await DataService.GetProStatus();
                if(status?.IsPro ?? false)
                {
                    SettingsService.IsPro = true;
                    SettingsService.NeedsProSync = false;
                    OnPropertyChanged(nameof(IsPro));
                    OnPropertyChanged(nameof(IsNotPro));
                    OnPropertyChanged(nameof(NeedsProSync));
                }
                else
                {
                    await DisplayAlert("Uh Oh!", $"No past pro status found. Please purchase Island Tracker Pro or contact support.");
                }
            }
            catch (HttpResponseException hrex) when (!string.IsNullOrWhiteSpace(hrex.Message))
            {
                await DisplayAlert("Uh oh, turbulence", hrex.Message);
            }
            catch (Exception ex)
            {
                await DisplayAlert("Uh oh, turbulence", "Looks like something went wrong. Check internet and try again.");

                Crashes.TrackError(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}


<|start_filename|>TurnipTracker.Functions/Pro/CreateProStatus.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using TurnipTracker.Functions.Helpers;
using TurnipTracker.Shared;
using System.Web.Http;
using TurnipTracker.Functions.Model;

namespace TurnipTracker.Functions.Pro
{
    public static class CreateProStatus
    {
        [FunctionName(nameof(CreateProStatus))]
        public static async Task<IActionResult> Run(
             [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
             [Table("ProStatus")] CloudTable proTable,
             ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(CreateProStatus)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new UnauthorizedResult();

            ProStatus status = null;

            try
            {
                var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
                status = JsonConvert.DeserializeObject<ProStatus>(requestBody);
            }
            catch (Exception ex)
            {
                log.LogError("Unable to deserialize pro status: " + ex.Message);
            }

            if (status == null ||
                string.IsNullOrWhiteSpace(status.PublicKey) ||
                !status.IsPro)
            {
                return new BadRequestErrorMessageResult("Invalid data to process request");
            }
           
            var proEntity = new ProStatusEntity(status.PublicKey, privateKey)
            {
                Receipt = status.Receipt ?? string.Empty
            };

            try
            {
                // Create the InsertOrReplace table operation
                var insertOrMergeOperation = TableOperation.InsertOrMerge(proEntity);

                // Execute the operation.
                var result = await proTable.ExecuteAsync(insertOrMergeOperation);
                var insertedPro = result.Result as ProStatusEntity;

            }
            catch (Exception ex)
            {
                log.LogError($"Error {nameof(CreateProStatus)} - Error: " + ex.Message);
                return new InternalServerErrorResult();
            }

            return new OkObjectResult("Pro Status Created");
        }
    }
}


<|start_filename|>TurnipTracker.Functions/FriendRequests/RejectFriendRequest.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.Web.Http;
using TurnipTracker.Shared;
using TurnipTracker.Functions.Helpers;

namespace TurnipTracker.Functions.FriendRequests
{
    public static class RejectFriendRequest
    {
        [FunctionName(nameof(RejectFriendRequest))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            [Table("FriendRequest")] CloudTable friendRequestTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(RejectFriendRequest)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new UnauthorizedResult();

            FriendRequest friendRequest = null;

            try
            {
                var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
                friendRequest = JsonConvert.DeserializeObject<FriendRequest>(requestBody);
            }
            catch (Exception ex)
            {
                log.LogError("Unable to deserialize friend request: " + ex.Message);

            }

            if (friendRequest == null ||
                string.IsNullOrWhiteSpace(friendRequest.MyPublicKey) ||
                string.IsNullOrWhiteSpace(friendRequest.FriendPublicKey))
            {
                return new BadRequestErrorMessageResult("Invalid data to process request");
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, friendRequest.MyPublicKey);
                if (user == null)
                    return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }
            catch (Exception ex)
            {
                log.LogInformation("User doesn't exist: " + ex.Message);
                return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }


            try
            {
                var myPublicKey = friendRequest.MyPublicKey;
                var friendPublicKey = friendRequest.FriendPublicKey;

                var removeOperation1 = TableOperation.Delete(new FriendRequestEntity
                {
                    PartitionKey = myPublicKey,
                    RowKey = friendPublicKey,
                    ETag = "*"
                });


                // Execute the operation.
                var result = await friendRequestTable.ExecuteAsync(removeOperation1);
                if (result == null)
                    return new InternalServerErrorResult();

            }
            catch (Exception ex)
            {
                log.LogInformation($"Error {nameof(RejectFriendRequest)} - Error: " + ex.Message);
                return new InternalServerErrorResult();
            }

            return new OkObjectResult("Friend Request Removed");
        }
    }
}


<|start_filename|>TurnipTracker.Shared/FriendRequest.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Shared
{
    public class FriendRequestCount
    {
        public int Count { get; set; }
    }
    public class FriendRequest
    {
        public string MyPublicKey { get; set; }
        public string FriendPublicKey { get; set; }
        public bool ProUser { get; set; }
    }

    public class MyPendingFriendRequest
    {
        public string MyPublicKey { get; set; }
    }

    public class MyFriendsRequest
    {
        public string MyPublicKey { get; set; }
    }
}


<|start_filename|>TurnipTracker/Model/GateStatus.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Model
{
    public enum GateStatus
    {
        Closed = 0,
        Everyone = 1,
        BestFriends = 2,
        DodoCode = 3
    }
}


<|start_filename|>TurnipTracker/Model/FriendGroup.cs<|end_filename|>
﻿using System.Collections.Generic;
using MvvmHelpers;
using TurnipTracker.Shared;

namespace TurnipTracker.Model
{
    public class FriendGroup : Grouping<string, FriendStatus>
    {
        public FriendGroup(string key, IEnumerable<FriendStatus> items) : base(key, items)
        {
        }
    }
}


<|start_filename|>TurnipTracker.Shared/Friend.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TurnipTracker.Shared
{
    public class Friend
    {
        public string MyPublicKey { get; set; }
        public string FriendPublicKey { get; set; }
    }
}


<|start_filename|>TurnipTracker/View/FriendRequestPage.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TurnipTracker.ViewModel;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Xamarin.Forms.Xaml;

namespace TurnipTracker.View
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class FriendRequestPage : ContentPage
    {
        public FriendRequestPage()
        {
            InitializeComponent();
            On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(true);
        }


        protected override void OnSizeAllocated(double width, double height)
        {
            var safeInsets = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
            safeInsets.Bottom = 0;
            Padding = safeInsets;
            base.OnSizeAllocated(width, height);
        }

        protected override void OnAppearing()
        {
            ((FriendRequestViewModel)BindingContext).RefreshCommand.ExecuteAsync().ContinueWith((r) => { });
            base.OnAppearing();
        }

        protected override bool OnBackButtonPressed()
        {
            if (IsBusy)
                return false;

            return base.OnBackButtonPressed();
        }
    }
}

<|start_filename|>TurnipTracker/Converters/HasFriendsCodeConverter.cs<|end_filename|>
﻿using System;
using System.Globalization;
using Xamarin.Forms;

namespace TurnipTracker.Converters
{
    public class HasFriendsCodeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return false;
            if (value is string diff)
                return !string.IsNullOrWhiteSpace(diff) && diff.Length == 17;

            return false;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


<|start_filename|>TurnipTracker.Functions/FriendRequests/SubmitFriendRequest.cs<|end_filename|>
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using TurnipTracker.Shared;
using System.Web.Http;
using TurnipTracker.Functions.Helpers;
using System.Linq;

namespace TurnipTracker.Functions
{
    public static class SubmitFriendRequest
    {
        [FunctionName(nameof(SubmitFriendRequest))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            [Table("FriendRequest")] CloudTable friendRequestTable,
            [Table("Friend")] CloudTable friendTable,
            [Table("User")] CloudTable userTable,
            ILogger log)
        {
            log.LogInformation($"C# HTTP trigger {nameof(SubmitFriendRequest)} function processed a request.");


            var privateKey = Utils.ParseToken(req);
            if (privateKey == null)
                return new UnauthorizedResult();

            FriendRequest friendRequest = null;

            try
            {
                var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
                friendRequest = JsonConvert.DeserializeObject<FriendRequest>(requestBody);
            }
            catch (Exception ex)
            {
                log.LogInformation("Unable to deserialize user: " + ex.Message);

            }

            if (friendRequest == null ||
                string.IsNullOrWhiteSpace(friendRequest.MyPublicKey) ||
                string.IsNullOrWhiteSpace(friendRequest.FriendPublicKey))
            {
                return new BadRequestErrorMessageResult("Invalid data to process request");
            }

            try
            {
                if(await Utils.ReachedMaxFriends(friendTable, friendRequest.MyPublicKey, friendRequest.ProUser))
                {
                    if (friendRequest.ProUser)
                        return new BadRequestErrorMessageResult("You have reached the max friend count at this time.");
                    else
                        return new BadRequestErrorMessageResult("You have reached the max friend count at this time. Please upgrade to Island Tracker Pro.");
                }
            }
            catch (Exception ex)
            {
                log.LogInformation("Unable to count existing friends: " + ex.Message);
                return new InternalServerErrorResult();
            }

            try
            {
                var user = await Utils.FindUserEntitySlim(userTable, privateKey, friendRequest.MyPublicKey);
                if (user == null)
                    return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }
            catch (Exception ex)
            {
                log.LogInformation("User doesn't exist: " + ex.Message);
                return new BadRequestErrorMessageResult("Unable to locate your user account.");
            }

            try
            {
                var requester = friendRequest.MyPublicKey;
                var requestee = friendRequest.FriendPublicKey;
                if (await Utils.HasFriend(friendTable, requester, requestee))
                    return new BadRequestErrorMessageResult("You are already friends!");

                // Create the InsertOrReplace table operation
                var insertOperation = TableOperation.InsertOrMerge(new FriendRequestEntity
                {
                    PartitionKey = requestee,
                    RowKey = requester
                });

                var result = await friendRequestTable.ExecuteAsync(insertOperation);
                if (result == null)
                    return new InternalServerErrorResult();

            }
            catch (Exception ex)
            {
                log.LogInformation($"Error {nameof(SubmitFriendRequest)} - Error: " + ex.Message);
                return new InternalServerErrorResult();
            }

            return new OkObjectResult("Friend Request Created");
        }
    }
}
