feat:onboarding flow for new users
This commit is contained in:
@@ -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]));
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -9,7 +9,7 @@ use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class InvitedToPortfolioNotification extends Notification implements ShouldQueue
|
||||
class InvitedOnboardingNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@@ -37,13 +37,15 @@ class InvitedToPortfolioNotification extends Notification implements ShouldQueue
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
|
||||
$url = url()->signedRoute('invited_onboarding', ['portfolio' => $this->portfolio->id, 'user' => $notifiable->id], now()->addDays(90));
|
||||
|
||||
return (new MailMessage)
|
||||
->replyTo($this->sender->email, $this->sender->name)
|
||||
->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('Once you\'re in, you\'ll be able to see holdings, dividends, market performance and more!')
|
||||
->action("Get Started", route('portfolio.show', ['portfolio' => $this->portfolio->id]))
|
||||
->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", $url)
|
||||
->line("If you have any questions, you can reply to this email.")
|
||||
->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);
|
||||
$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)
|
||||
->greeting('Welcome back!')
|
||||
->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:")
|
||||
->action("Connect $provider", route('oauth.verify_connected_account', ['verification_id' => $this->verification_id]))
|
||||
->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.');
|
||||
->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. This link will expire in {$days} days.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-1
@@ -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",
|
||||
"Share": "Share",
|
||||
"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
@@ -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",
|
||||
"Share": "Compartir",
|
||||
"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 class="flex items-center justify-end mt-4">
|
||||
<x-button class="btn-primary">
|
||||
<x-button class="btn-primary" type="submit">
|
||||
{{ __('Email Password Reset Link') }}
|
||||
</x-button>
|
||||
</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>
|
||||
@@ -51,29 +51,15 @@
|
||||
|
||||
<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
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-button class="btn-primary">
|
||||
<x-button class="btn-primary" type="submit">
|
||||
{{ __('Reset Password') }}
|
||||
</x-button>
|
||||
</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>
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
</x-ib-card>
|
||||
|
||||
@if (!$user->portfolios->isEmpty())
|
||||
@if (!$user->transactions->isEmpty())
|
||||
<x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3">
|
||||
|
||||
@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 Illuminate\Support\Collection;
|
||||
use Mary\Traits\Toast;
|
||||
use App\Notifications\InvitedToPortfolioNotification;
|
||||
use App\Notifications\InvitedOnboardingNotification;
|
||||
|
||||
new class extends Component {
|
||||
|
||||
@@ -105,7 +105,7 @@ new class extends Component {
|
||||
if (!empty($sync['attached'])) {
|
||||
|
||||
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
|
||||
:item="$user"
|
||||
avatar="profile_photo_url"
|
||||
value="name"
|
||||
no-separator
|
||||
class="!-my-2 rounded"
|
||||
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>
|
||||
{{ $user->email }}
|
||||
|
||||
</x-slot:sub-value>
|
||||
<x-slot:actions>
|
||||
@if (auth()->user()->id != $user->id)
|
||||
<x-select
|
||||
class="select select-ghost border-none focus:outline-none focus:ring-0"
|
||||
:options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]"
|
||||
wire:model.live.number="permissions.{{ $user->id }}.full_access"
|
||||
/>
|
||||
@if($user->id != auth()->user()->id)
|
||||
|
||||
<x-button
|
||||
class="btn-sm btn-ghost btn-circle"
|
||||
wire:click="deleteUser('{{ $user->id }}')"
|
||||
spinner="deleteUser"
|
||||
spinner="deleteUser('{{ $user->id }}')"
|
||||
>
|
||||
<x-icon name="o-x-mark" class="w-4" />
|
||||
</x-button>
|
||||
</x-button>
|
||||
@endif
|
||||
|
||||
</x-slot:actions>
|
||||
</x-list-item>
|
||||
@endforeach
|
||||
|
||||
+5
-1
@@ -6,6 +6,7 @@ use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\PortfolioController;
|
||||
use App\Http\Controllers\ConnectedAccountController;
|
||||
use App\Http\Controllers\TransactionController;
|
||||
use App\Http\Controllers\InvitedOnboardingController;
|
||||
use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController;
|
||||
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');
|
||||
});
|
||||
|
||||
// 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('/privacy', [PrivacyPolicyController::class, 'show'])->name('policy.show');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user