feat:onboarding flow for new users

This commit is contained in:
hackerESQ
2024-10-22 20:29:54 -05:00
parent 5756fa06d7
commit b6a123a90f
14 changed files with 184 additions and 41 deletions
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Portfolio;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class InvitedOnboardingController extends Controller
{
/**
* Check if the invited user needs a password?
*
*/
public function __invoke(Request $request, Portfolio $portfolio, User $user)
{
if (!$request->hasValidSignature()) {
abort(401, 'Invalid signature');
}
// user doesn't have password
if (is_null($user->password)) {
// route to create password form
return view('auth.invited-onboarding', [
'portfolio' => $portfolio,
'user' => $user
]);
}
// redirect user to portfolio
return redirect(route('portfolio.show', ['portfolio' => $portfolio->id]));
}
}
@@ -9,7 +9,7 @@ use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class InvitedToPortfolioNotification extends Notification implements ShouldQueue class InvitedOnboardingNotification extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -37,13 +37,15 @@ class InvitedToPortfolioNotification extends Notification implements ShouldQueue
public function toMail(object $notifiable): MailMessage public function toMail(object $notifiable): MailMessage
{ {
$url = url()->signedRoute('invited_onboarding', ['portfolio' => $this->portfolio->id, 'user' => $notifiable->id], now()->addDays(90));
return (new MailMessage) return (new MailMessage)
->replyTo($this->sender->email, $this->sender->name) ->replyTo($this->sender->email, $this->sender->name)
->greeting('Hey there! 👋') ->greeting('Hey there! 👋')
->subject("{$this->sender->name} invited you to {$this->portfolio->title} on Investbrain!") ->subject("You've been invited to {$this->portfolio->title} on Investbrain!")
->line("{$this->sender->name} has invited you to **{$this->portfolio->title}** on Investbrain, Smart open-source investment tracker that consolidates and monitors market performance across your different brokerages.") ->line("{$this->sender->name} has invited you to **{$this->portfolio->title}** on Investbrain, Smart open-source investment tracker that consolidates and monitors market performance across your different brokerages.")
->line('Once you\'re in, you\'ll be able to see holdings, dividends, market performance and more!') ->line("Once you're in, you'll be able to see all the holdings, dividends, market performance and more for {$this->portfolio->title}!")
->action("Get Started", route('portfolio.show', ['portfolio' => $this->portfolio->id])) ->action("Get Started", $url)
->line("If you have any questions, you can reply to this email.") ->line("If you have any questions, you can reply to this email.")
->salutation("See you there,\n". e($this->sender->name)); ->salutation("See you there,\n". e($this->sender->name));
} }
@@ -37,12 +37,14 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ
$verification = ConnectedAccountVerification::find($this->verification_id); $verification = ConnectedAccountVerification::find($this->verification_id);
$provider = config("services.$verification->provider.name"); $provider = config("services.$verification->provider.name");
$url = url()->signedRoute('oauth.verify_connected_account', ['verification_id' => $this->verification_id], now()->days($days = 7));
return (new MailMessage) return (new MailMessage)
->greeting('Welcome back!') ->greeting('Welcome back!')
->subject("Connect your $provider account with Investbrain") ->subject("Connect your $provider account with Investbrain")
->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:") ->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:")
->action("Connect $provider", route('oauth.verify_connected_account', ['verification_id' => $this->verification_id])) ->action("Connect $provider", $url)
->line('If you do not recognize this activity, we recommend [changing your password]('.route('profile.show').') as soon as possible. Otherwise, you can disregard this message.'); ->line("If you do not recognize this activity, we recommend [changing your password](".route('profile.show').") as soon as possible. Otherwise, you can disregard this message. This link will expire in {$days} days.");
} }
/** /**
+7 -1
View File
@@ -354,5 +354,11 @@
"Allow this user to manage portfolio details and create or update transactions": "Allow this user to manage portfolio details and create or update transactions", "Allow this user to manage portfolio details and create or update transactions": "Allow this user to manage portfolio details and create or update transactions",
"Share": "Share", "Share": "Share",
"Remove Access": "Remove Access", "Remove Access": "Remove Access",
"By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately.": "By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately." "By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately.": "By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately.",
"Hey again!": "Hey again!",
"Before you can get started with Investbrain, you'll want to create a password:": "Before you can get started with Investbrain, you'll want to create a password:",
"Or login with SSO:": "Or login with SSO:",
"Create Password": "Create Password"
} }
+6 -1
View File
@@ -354,5 +354,10 @@
"Allow this user to manage portfolio details and create or update transactions": "Permitir a este usuario administrar detalles de portafolio y crear o actualizar transacciones", "Allow this user to manage portfolio details and create or update transactions": "Permitir a este usuario administrar detalles de portafolio y crear o actualizar transacciones",
"Share": "Compartir", "Share": "Compartir",
"Remove Access": "Eliminar acceso", "Remove Access": "Eliminar acceso",
"By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately.": "Al eliminar el acceso de esta persona, ya no podrá ver este portafolio. Perderán el acceso inmediatamente." "By removing this person's access, they will no longer be able to view this portfolio. They will lose access immediately.": "Al eliminar el acceso de esta persona, ya no podrá ver este portafolio. Perderán el acceso inmediatamente.",
"Hey again!": "¡Oye de nuevo!",
"Before you can get started with Investbrain, you'll want to create a password:": "Antes de poder comenzar a utilizar Investbrain, deberá crear una cuenta:",
"Or login with SSO:": "O iniciar sesión mediante SSO:",
"Create Password": "Crear Contraseña"
} }
@@ -27,7 +27,7 @@
</div> </div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<x-button class="btn-primary"> <x-button class="btn-primary" type="submit">
{{ __('Email Password Reset Link') }} {{ __('Email Password Reset Link') }}
</x-button> </x-button>
</div> </div>
@@ -0,0 +1,24 @@
<x-guest-layout>
<x-authentication-card>
<x-slot:logo>
<div class="w-24 mb-10">
<x-glyph-only-logo />
</div>
</x-slot:logo>
<h1 class="text-2xl font-bold mb-4">{{ __('Hey again!') }} 👋</h1>
<p class="mb-2">{{ __('Before you can get started with Investbrain, you\'ll want to create a password:') }}</p>
@livewire('invited-onboarding-form', [
'portfolio' => $portfolio,
'user' => $user,
])
<x-section-border />
<p class="mb-4">{{ __('Or login with SSO:') }}</p>
<x-connected-accounts-login />
</x-authentication-card>
</x-guest-layout>
+8 -22
View File
@@ -51,29 +51,15 @@
<x-section-border /> <x-section-border />
<div class=""> <x-connected-accounts-login />
<x-button
link="{{ route('register') }}"
class="btn-sm btn-block btn-outline btn-secondary my-1"
>
{{ __('Sign up with email') }}
</x-button>
@foreach(explode(',', config('services.enabled_login_providers')) as $provider)
<x-button
link="{{ route('oauth.redirect', ['provider' => $provider]) }}"
class="btn-sm btn-block my-1"
style='background-color: {{ config("services.$provider.color") }}'
no-wire-navigate
>
@include("components.$provider-icon")
{{ __('Login with') }} {{ config("services.$provider.name") }}
</x-button>
@endforeach
<x-button
link="{{ route('register') }}"
class="btn-sm btn-block btn-outline btn-secondary my-1"
>
{{ __('Sign up with email') }}
</x-button>
</div>
@endif @endif
</form> </form>
</x-authentication-card> </x-authentication-card>
@@ -29,7 +29,7 @@
</div> </div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<x-button class="btn-primary"> <x-button class="btn-primary" type="submit">
{{ __('Reset Password') }} {{ __('Reset Password') }}
</x-button> </x-button>
</div> </div>
@@ -0,0 +1,14 @@
<div>
@foreach(explode(',', config('services.enabled_login_providers')) as $provider)
<x-button
link="{{ route('oauth.redirect', ['provider' => $provider]) }}"
class="btn-sm btn-block my-1"
style='background-color: {{ config("services.$provider.color") }}'
no-wire-navigate
>
@include("components.$provider-icon")
{{ __('Login with') }} {{ config("services.$provider.name") }}
</x-button>
@endforeach
</div>
+1 -1
View File
@@ -59,7 +59,7 @@
</x-ib-card> </x-ib-card>
@if (!$user->portfolios->isEmpty()) @if (!$user->transactions->isEmpty())
<x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3"> <x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3">
@livewire('transactions-list', [ @livewire('transactions-list', [
@@ -0,0 +1,55 @@
<?php
use App\Models\Portfolio;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
new class extends Component {
// props
public Portfolio $portfolio;
public User $user;
#[Rule('required|string|confirmed')]
public string $password;
#[Rule('required|string')]
public string $password_confirmation;
// methods
public function updatePassword()
{
$this->validate();
$this->user->password = Hash::make($this->password);
$this->user->save();
Auth::login($this->user, true);
return redirect(route('portfolio.show', ['portfolio' => $this->portfolio->id]));
}
}; ?>
<x-form wire:submit="updatePassword" class="">
<div class="mt-2">
<x-input wire:model="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" autofocus />
</div>
<div class="mt-2">
<x-input wire:model="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" />
</div>
<div class="flex items-center justify-end mt-2">
<x-button class="btn-primary" type="submit">
{{ __('Create Password') }}
</x-button>
</div>
</x-form>
@@ -6,7 +6,7 @@ use Livewire\Attributes\Rule;
use Livewire\Volt\Component; use Livewire\Volt\Component;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Mary\Traits\Toast; use Mary\Traits\Toast;
use App\Notifications\InvitedToPortfolioNotification; use App\Notifications\InvitedOnboardingNotification;
new class extends Component { new class extends Component {
@@ -105,7 +105,7 @@ new class extends Component {
if (!empty($sync['attached'])) { if (!empty($sync['attached'])) {
foreach($sync['attached'] as $newUserId) { foreach($sync['attached'] as $newUserId) {
User::find($newUserId)->notify(new InvitedToPortfolioNotification($this->portfolio, auth()->user())); User::find($newUserId)->notify(new InvitedOnboardingNotification($this->portfolio, auth()->user()));
}; };
} }
@@ -150,30 +150,38 @@ new class extends Component {
<x-list-item <x-list-item
:item="$user" :item="$user"
avatar="profile_photo_url" avatar="profile_photo_url"
value="name"
no-separator no-separator
class="!-my-2 rounded" class="!-my-2 rounded"
x-data="{ loading: false, timeout: null }" x-data="{ loading: false, timeout: null }"
> >
<x-slot:value>
{{ $user->name }}
@if (auth()->user()->id == $user->id)
({{ __('you') }})
@endif
</x-slot:value>
<x-slot:sub-value> <x-slot:sub-value>
{{ $user->email }} {{ $user->email }}
</x-slot:sub-value> </x-slot:sub-value>
<x-slot:actions> <x-slot:actions>
@if (auth()->user()->id != $user->id)
<x-select <x-select
class="select select-ghost border-none focus:outline-none focus:ring-0" class="select select-ghost border-none focus:outline-none focus:ring-0"
:options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]" :options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]"
wire:model.live.number="permissions.{{ $user->id }}.full_access" wire:model.live.number="permissions.{{ $user->id }}.full_access"
/> />
@if($user->id != auth()->user()->id)
<x-button <x-button
class="btn-sm btn-ghost btn-circle" class="btn-sm btn-ghost btn-circle"
wire:click="deleteUser('{{ $user->id }}')" wire:click="deleteUser('{{ $user->id }}')"
spinner="deleteUser" spinner="deleteUser('{{ $user->id }}')"
> >
<x-icon name="o-x-mark" class="w-4" /> <x-icon name="o-x-mark" class="w-4" />
</x-button> </x-button>
@endif @endif
</x-slot:actions> </x-slot:actions>
</x-list-item> </x-list-item>
@endforeach @endforeach
+5 -1
View File
@@ -6,6 +6,7 @@ use App\Http\Controllers\DashboardController;
use App\Http\Controllers\PortfolioController; use App\Http\Controllers\PortfolioController;
use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\ConnectedAccountController;
use App\Http\Controllers\TransactionController; use App\Http\Controllers\TransactionController;
use App\Http\Controllers\InvitedOnboardingController;
use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController;
use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController; use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController;
@@ -35,7 +36,10 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->group(fun
Route::get('/transactions', [TransactionController::class, 'index'])->name('transaction.index'); Route::get('/transactions', [TransactionController::class, 'index'])->name('transaction.index');
}); });
// overwrites jetstream routes // Invited onboarding
Route::get('invite/{portfolio}/{user}', InvitedOnboardingController::class)->name('invited_onboarding')->scopeBindings();
// Overwrites Jetstream routes
Route::get('/terms', [TermsOfServiceController::class, 'show'])->name('terms.show'); Route::get('/terms', [TermsOfServiceController::class, 'show'])->name('terms.show');
Route::get('/privacy', [PrivacyPolicyController::class, 'show'])->name('policy.show'); Route::get('/privacy', [PrivacyPolicyController::class, 'show'])->name('policy.show');