Setup account linking acceptance view

This commit is contained in:
Alexandru Macocian
2025-11-06 23:59:41 +02:00
parent dfd5529cf0
commit 39591ab84a
9 changed files with 294 additions and 13 deletions
+11 -1
View File
@@ -335,7 +335,17 @@ class SettingsController extends Controller
return redirect()->route('settings.subscriptions.index');
}
return view('settings.users.link');
// Get invitations sent by current user's account
$sentInvitations = auth()->user()->account->accountLinks()
->with(['user', 'invitedBy'])
->get();
// Get invitations received by current user
$receivedInvitations = AccountLink::where('user_id', auth()->user()->id)
->with(['account', 'invitedBy'])
->get();
return view('settings.users.link', compact('sentInvitations', 'receivedInvitations'));
}
/**
+1
View File
@@ -327,6 +327,7 @@ common.loadLanguage(window.Laravel.locale, true).then((i18n) => {
locale: i18n.locale,
reminders_frequency: 'once',
accept_invite_user: false,
accept_link_user: false,
date_met_the_contact: 'known',
global_relationship_form_new_contact: true,
global_profile_default_view: window.Laravel.profileDefaultView,
+7
View File
@@ -50,4 +50,11 @@ return [
'export_description' => 'You requested a data export on :date. It is now ready to download.',
'export_download' => 'Download export',
'account_link_title' => 'Monica Account Link Invitation from :name',
'account_link_intro' => ':name (:email) has invited you to link your Monica account with theirs.',
'account_link_explanation' => 'By accepting this invitation, both accounts will share the same contact data and CRM information. You will be able to access and manage the same contacts, relationships, and data.',
'account_link_link' => 'To accept the account link invitation, click on the link below:',
'account_link_button' => 'Accept Account Link',
'account_link_expiration' => 'This link will expire in :count days.',
];
+39
View File
@@ -197,6 +197,45 @@ return [
'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?',
'users_invitation_need_subscription' => 'Adding more users requires a subscription.',
// Account linking
'users_list_link_user' => 'Link existing user',
'users_link_breadcrumb' => 'Link existing user',
'users_link_title' => 'Link an existing Monica user to your account',
'users_link_description' => 'Invite an existing Monica user to link their account with yours. Both accounts will share the same contact data and CRM information.',
'users_link_email_field' => 'Enter the email of the existing Monica user',
'users_link_submit' => 'Send invitation',
'users_link_pending_title' => 'Pending account link invitations',
'users_link_pending_description' => 'These are account link invitations you\'ve sent that haven\'t been accepted yet.',
'users_link_no_pending' => 'No pending account link invitations.',
'users_link_invited_by' => 'Invited by :user',
'users_link_invited_on' => 'Sent on :date',
'users_link_cancel_confirm' => 'Are you sure you want to cancel this account link invitation?',
'users_link_confirmation' => 'Please confirm that you want to invite this user before proceeding with the invitation',
'users_link_sent_title' => 'Invitations you\'ve sent',
'users_link_sent_description' => 'Account link invitations you\'ve sent that are awaiting response.',
'users_link_received_title' => 'Invitations you\'ve received',
'users_link_received_description' => 'Account link invitations others have sent to you.',
'users_link_no_sent' => 'No outgoing invitations.',
'users_link_no_received' => 'No incoming invitations.',
'users_link_status_pending' => 'Pending',
'users_link_status_awaiting_response' => 'Awaiting your response',
'account_link_decline_confirm' => 'Are you sure you want to decline this invitation?',
'account_link_title' => 'Link Your Account',
'account_link_invitation_from' => 'Invitation from :name',
'account_link_invitation_description' => 'You have been invited to link your Monica account with account #:account. If you accept, both accounts will share the same contact data.',
'account_link_what_happens_title' => 'What happens when you accept:',
'account_link_what_happens_1' => 'Your current account data will be merged with the inviter\'s account',
'account_link_what_happens_2' => 'You will have access to all contacts and data in the linked account',
'account_link_what_happens_3' => 'This action cannot be undone',
'account_link_accept' => 'Accept invitation',
'account_link_decline' => 'Decline',
'account_link_back_to_dashboard' => 'Back to dashboard',
'account_link_login_required' => 'Please login to accept this account link invitation.',
'account_link_wrong_user' => 'This invitation is not for your account.',
'account_link_success' => 'Account successfully linked! You now have access to the shared account data.',
'account_link_declined' => 'Account link invitation declined.',
'subscriptions_account_current_plan' => 'Your current plan',
'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:',
'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.',
@@ -0,0 +1,61 @@
@extends('marketing.skeleton')
@section('content')
<body class="marketing register">
<div class="container">
<div class="row">
<div class="col-12 col-md-6 offset-md-3 offset-md-3-right">
<div class="signup-box">
<div class="dt w-100">
<div class="dtc tc">
<img src="{{ asset('img/monica.svg') }}" width="97" height="88" alt="">
</div>
</div>
<h2>{{ trans('settings.account_link_title') }}</h2>
@include ('partials.errors')
<div class="alert alert-info">
<strong>{{ trans('settings.account_link_invitation_from', ['name' => $accountLink->invitedBy->first_name]) }}</strong>
<p class="mb-0 mt-2">{{ trans('settings.account_link_invitation_description', ['account' => $accountLink->account->id]) }}</p>
</div>
<div class="card">
<div class="card-body">
<h5>{{ trans('settings.account_link_what_happens_title') }}</h5>
<ul class="mb-0">
<li>{{ trans('settings.account_link_what_happens_1') }}</li>
<li>{{ trans('settings.account_link_what_happens_2') }}</li>
<li>{{ trans('settings.account_link_what_happens_3') }}</li>
</ul>
</div>
</div>
<div class="form-group actions mt-4">
<form method="POST" action="{{ route('account-links.send', $key) }}" style="display: inline-block; width: 48%; margin-right: 4%;">
@csrf
{{-- Security field to verify inviter's email --}}
<input type="hidden" name="email_security" value="{{ $accountLink->invitedBy->email }}">
<button type="submit" class="btn btn-primary btn-block">{{ trans('settings.account_link_accept') }}</button>
</form>
<form method="POST" action="{{ route('account-links.decline', $key) }}" style="display: inline-block; width: 48%;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-outline-secondary btn-block">{{ trans('settings.account_link_decline') }}</button>
</form>
</div>
<div class="form-group links mt-4">
<ul>
<li><a href="{{ route('dashboard.index') }}">{{ trans('settings.account_link_back_to_dashboard') }}</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</body>
@endsection
@@ -43,7 +43,11 @@
<p>{{ trans('settings.users_blank_description') }}</p>
<p class="cta"><a href="{{ route('settings.users.create') }}" class="btn">{{ trans('settings.users_blank_cta') }}</a></p>
<p class="cta">
<a href="{{ route('settings.users.create') }}" class="btn">{{ trans('settings.users_blank_cta') }}</a>
<span class="ml-2">{{ trans('app.or') }}</span>
<a href="{{ route('settings.users.link.create') }}" class="btn btn-outline-primary ml-2">{{ trans('settings.users_list_link_user') }}</a>
</p>
@if (config('monica.requires_subscription') && $accountHasLimitations)
<p class="requires-subscription">{{ trans('settings.users_invitation_need_subscription') }}</p>
@@ -36,7 +36,10 @@
<div class="pa3 bb b--gray-monica">
<h3 class="with-actions">
{{ trans('settings.users_list_title') }}
<a href="{{ route('settings.users.create') }}" class="btn">{{ trans('settings.users_list_add_user') }}</a>
<div class="actions-list">
<a href="{{ route('settings.users.create') }}" class="btn">{{ trans('settings.users_list_add_user') }}</a>
<a href="{{ route('settings.users.link.create') }}" class="btn btn-outline-primary ml-2">{{ trans('settings.users_list_link_user') }}</a>
</div>
</h3>
<ul class="table">
@foreach ($users as $user)
@@ -0,0 +1,151 @@
@extends('layouts.skeleton')
@section('content')
<div class="settings">
{{-- Breadcrumb --}}
<div class="breadcrumb">
<div class="{{ Auth::user()->getFluidLayout() }}">
<div class="row">
<div class="col-12">
<ul class="horizontal">
<li>
<a href="{{ route('dashboard.index') }}">{{ trans('app.breadcrumb_dashboard') }}</a>
</li>
<li>
<a href="{{ route('settings.index') }}">{{ trans('app.breadcrumb_settings') }}</a>
</li>
<li>
<a href="{{ route('settings.users.index') }}">{{ trans('app.breadcrumb_settings_users') }}</a>
</li>
<li>
{{ trans('settings.users_link_breadcrumb') }}
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Page content -->
<div class="main-content central-form">
<div class="{{ Auth::user()->getFluidLayout() }}">
<div class="row">
<div class="col-12 col-sm-6 offset-sm-3 offset-sm-3-right">
<div class="br3 ba b--gray-monica bg-white mb4">
<div class="pa3 bb b--gray-monica">
<form id="linkForm" method="POST" action="{{ route('settings.users.link.store') }}">
@csrf
<h2>{{ trans('settings.users_link_title') }}</h2>
<p>{{ trans('settings.users_link_description') }}</p>
@include('partials.errors')
{{-- Email --}}
<fieldset class="form-group">
<div class="form-group">
<label for="email">{{ trans('settings.users_link_email_field') }}</label>
<input type="email" class="form-control" name="email" value="{{ old('email') }}" required autofocus>
</div>
</fieldset>
{{-- Explicit confirmation --}}
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="confirmation" name="confirmation" value="1" required>
<label class="form-check-label" for="confirmation">
{{ trans('settings.users_link_confirmation') }}
</label>
</div>
</div>
{{-- Submit button inside form --}}
<div class="form-group">
<button type="submit" class="btn btn-primary">{{ trans('settings.users_link_submit') }}</button>
<a href="{{ route('settings.users.index') }}" class="btn btn-secondary">{{ trans('app.cancel') }}</a>
</div>
</form>
</div>
</div>
{{-- Invitations you've sent --}}
<div class="br3 ba b--gray-monica bg-white mb4">
<div class="pa3 bb b--gray-monica">
<h3>{{ trans('settings.users_link_sent_title') }}</h3>
<p>{{ trans('settings.users_link_sent_description') }}</p>
</div>
<div class="pa3">
@if($sentInvitations->count() > 0)
<ul class="list-group">
@foreach($sentInvitations as $link)
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ $link->user->email }}</strong>
<small class="text-muted d-block">{{ trans('settings.users_link_invited_on', ['date' => $link->created_at->format('M j, Y')]) }}</small>
<span class="badge badge-warning">{{ trans('settings.users_link_status_pending') }}</span>
</div>
<form method="POST" action="{{ route('settings.users.link.delete', $link) }}" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('{{ trans('settings.users_link_cancel_confirm') }}')">
{{ trans('app.cancel') }}
</button>
</form>
</li>
@endforeach
</ul>
@else
<p class="text-muted">{{ trans('settings.users_link_no_sent') }}</p>
@endif
</div>
</div>
{{-- Invitations you've received --}}
<div class="br3 ba b--gray-monica bg-white mb4">
<div class="pa3 bb b--gray-monica">
<h3>{{ trans('settings.users_link_received_title') }}</h3>
<p>{{ trans('settings.users_link_received_description') }}</p>
</div>
<div class="pa3">
@if($receivedInvitations->count() > 0)
<ul class="list-group">
@foreach($receivedInvitations as $link)
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ trans('settings.account_link_invitation_from', ['name' => $link->invitedBy->first_name]) }}</strong>
<small class="text-muted d-block">{{ $link->invitedBy->email }}</small>
<small class="text-muted">{{ trans('settings.users_link_invited_on', ['date' => $link->created_at->format('M j, Y')]) }}</small>
<span class="badge badge-info">{{ trans('settings.users_link_status_awaiting_response') }}</span>
</div>
<div>
<a href="{{ route('account-links.accept', $link->invitation_key) }}" class="btn btn-sm btn-success mr-1">
{{ trans('settings.account_link_accept') }}
</a>
<form method="POST" action="{{ route('account-links.decline', $link->invitation_key) }}" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('{{ trans('settings.account_link_decline_confirm') }}')">
{{ trans('settings.account_link_decline') }}
</button>
</form>
</div>
</li>
@endforeach
</ul>
@else
<p class="text-muted">{{ trans('settings.users_link_no_received') }}</p>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
+15 -10
View File
@@ -126,6 +126,15 @@ RUN set -ex; \
\
echo 'memory_limit=512M' > $PHP_INI_DIR/conf.d/memory-limit.ini
# Install Node.js early to leverage Docker layer caching
RUN set -ex; \
\
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get install -y nodejs; \
npm install -g yarn; \
\
rm -rf /var/lib/apt/lists/*
RUN set -ex; \
\
a2enmod headers rewrite remoteip; \
@@ -144,7 +153,6 @@ RUN set -ex; \
WORKDIR /var/www/html
# Copy the local (outside Docker) source into the working directory,
# copy system files into their proper homes, and set file ownership
# correctly
@@ -171,7 +179,7 @@ RUN set -ex; \
php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer; \
rm composer-setup.php
# Install composer dependencies
# Install PHP dependencies
RUN set -ex; \
\
mkdir -p storage/framework/views; \
@@ -179,16 +187,13 @@ RUN set -ex; \
composer clear-cache; \
rm -rf .composer
# Install node dependencies
# Install Node.js dependencies and build assets
RUN set -ex; \
\
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get install -y nodejs; \
npm install -g yarn; \
yarn run inst; \
yarn run production; \
\
rm -rf /var/lib/apt/lists/*
yarn install --production=false; \
yarn run production
COPY scripts/docker/entrypoint.sh \
scripts/docker/cron.sh \