-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wallet: Implement recovery phrase confirmation page
- Loading branch information
1 parent
c1dce9f
commit 113eeca
Showing
5 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
@attribute [Route(Routes.WalletSeedConfirm)] | ||
@using BTCPayApp.Core | ||
@using BTCPayApp.Core.Auth | ||
@using BTCPayApp.Core.Contracts | ||
@using BTCPayApp.Core.Data | ||
@using BTCPayApp.Core.Wallet | ||
@using BTCPayApp.UI.Components.Layout | ||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent | ||
@inject ConfigProvider ConfigProvider | ||
@inject OnChainWalletManager OnChainWalletManager | ||
|
||
<PageTitle>Confirm your recovery phrase</PageTitle> | ||
|
||
<SectionContent SectionId="_Layout.Top"> | ||
<Titlebar Back> | ||
<h1>Confirm your recovery phrase</h1> | ||
</Titlebar> | ||
</SectionContent> | ||
|
||
<section class="container"> | ||
<AuthorizeView Policy="@AppPolicies.CanModifySettings"> | ||
<Authorized> | ||
<p class="text-center"> | ||
Match the word to the number to verify. | ||
</p> | ||
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@_errorMessage"> | ||
@if (Words is not null) | ||
{ | ||
<div class="box my-4"> | ||
<ol class="ask mt-3 mb-5"> | ||
@for (var i = 0; i < _ask.Length; i++) | ||
{ | ||
var num = _ask[i]; | ||
<li value="@num"> | ||
<div class="rounded-pill@(Model.Words.Count == i ? " current" : "")"> | ||
@(Model.Words.Count > i ? Model.Words[i] : "...") | ||
</div> | ||
</li> | ||
} | ||
</ol> | ||
<div class="words"> | ||
@foreach (var word in Shuffled) | ||
{ | ||
<button type="button" class="btn bg-white rounded-pill" @onclick="() => AddWord(word)">@word</button> | ||
} | ||
</div> | ||
</div> | ||
} | ||
@if (!Model.IsVerified) | ||
{ | ||
<button class="btn btn-primary w-100 rounded-pill" type="submit" disabled="@(Model.Words.Count != _ask.Length)"> | ||
<span>Verify recovery phrase</span> | ||
</button> | ||
} | ||
</ValidationEditContext> | ||
</Authorized> | ||
<NotAuthorized> | ||
<Alert Type="danger">Unauthorized.</Alert> | ||
</NotAuthorized> | ||
</AuthorizeView> | ||
</section> | ||
|
||
@code { | ||
private string? _errorMessage; | ||
private string? _successMessage; | ||
private BTCPayAppConfig? _config; | ||
private ValidationEditContext? _validationEditContext; | ||
VerificationModel Model { get; set; } = new(); | ||
|
||
private WalletConfig? Wallet { get; set; } | ||
private string[]? Words { get; set; } | ||
private string[]? Shuffled { get; set; } | ||
|
||
private readonly int[] _ask = [ | ||
Random.Shared.Next(1, 3), | ||
Random.Shared.Next(3, 5), | ||
Random.Shared.Next(5, 7), | ||
Random.Shared.Next(7, 9), | ||
Random.Shared.Next(9, 11), | ||
Random.Shared.Next(11, 13) | ||
]; | ||
|
||
protected override async Task OnInitializedAsync() | ||
{ | ||
await base.OnInitializedAsync(); | ||
|
||
_config = await ConfigProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig(); | ||
|
||
Wallet = await OnChainWalletManager.GetConfig(); | ||
Words = Wallet?.Mnemonic.Split(' '); | ||
Shuffled = Words?.OrderBy(_ => Guid.NewGuid()).ToArray(); | ||
} | ||
|
||
private async Task HandleValidSubmit() | ||
{ | ||
_errorMessage = _successMessage = null; | ||
Model.IsVerified = false; | ||
|
||
if (Words is null) | ||
{ | ||
_errorMessage = "Recovery phrase not available"; | ||
} | ||
else if (Model.Words.Count != _ask.Length) | ||
{ | ||
_errorMessage = "Please fill all words."; | ||
} | ||
else | ||
{ | ||
for (var i = 0; i < _ask.Length; i++) | ||
{ | ||
var num = _ask[i] - 1; | ||
var word = Model.Words[i]; | ||
var expected = Words[num]; | ||
if (word != expected) | ||
{ | ||
_errorMessage = "Please check the words."; | ||
Model.Words = []; | ||
return; | ||
} | ||
} | ||
|
||
_successMessage = "Good job, these are correct!"; | ||
Model.IsVerified = true; | ||
|
||
if (!_config!.RecoveryPhraseVerified) | ||
{ | ||
_config!.RecoveryPhraseVerified = true; | ||
await ConfigProvider.Set(BTCPayAppConfig.Key, _config, true); | ||
} | ||
} | ||
} | ||
|
||
private void AddWord(string word) | ||
{ | ||
if (Model.Words.Count < _ask.Length) | ||
Model.Words.Add(word); | ||
} | ||
|
||
private class VerificationModel | ||
{ | ||
public List<string> Words { get; set; } = []; | ||
public bool IsVerified { get; set; } | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
.container { | ||
max-width: 500px; | ||
} | ||
|
||
.ask { | ||
display: grid; | ||
grid-auto-flow: column; | ||
grid-template-rows: repeat(3, 50%); | ||
grid-template-columns: repeat(2, 50%); | ||
gap: var(--btcpay-space-m); | ||
margin-right: 2em; | ||
padding: 0; | ||
} | ||
|
||
.ask li::marker { | ||
color: var(--btcpay-body-text-muted); | ||
font-weight: var(--btcpay-font-weight-normal); | ||
} | ||
|
||
.ask li { | ||
margin-left: 2.5em; | ||
font-weight: var(--btcpay-font-weight-semibold); | ||
} | ||
|
||
.ask li .rounded-pill { | ||
display: block; | ||
border: 1px dashed var(--btcpay-body-border-medium); | ||
background: var(--btcpay-bg-tile); | ||
margin-left: var(--btcpay-space-xs); | ||
padding: .5rem 1.5rem; | ||
} | ||
|
||
.ask li .rounded-pill.current { | ||
border-color: 1px dashed var(--btcpay-body-border-light); | ||
} | ||
|
||
.words { | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: var(--btcpay-space-m); | ||
align-items: center; | ||
justify-content: space-evenly; | ||
} | ||
|
||
.words .btn { | ||
flex: 1 1 calc(50% - var(--btcpay-space-m)); | ||
--btcpay-btn-color: var(--btcpay-black); | ||
--btcpay-btn-hover-color: var(--btcpay-black); | ||
--btcpay-btn-active-color: var(--btcpay-black); | ||
--btcpay-btn-bg: var(--btcpay-white); | ||
} | ||
|
||
@media (min-width: 400px) { | ||
.words .btn { | ||
flex: 1 1 calc(33% - var(--btcpay-space-m)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters