diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -7,6 +7,7 @@
 use App\Discount;
 use App\Domain;
 use App\Plan;
+use App\Providers\PaymentProvider;
 use App\Rules\SignupExternalEmail;
 use App\Rules\SignupToken;
 use App\Rules\Password;
@@ -34,27 +35,25 @@
      */
     public function plans(Request $request)
     {
-        $plans = [];
-
         // Use reverse order just to have individual on left, group on right ;)
         // But prefer monthly on left, yearly on right
-        Plan::withEnvTenantContext()->orderBy('months')->orderByDesc('title')->get()
-            ->map(function ($plan) use (&$plans) {
-                // Allow themes to set custom button label
-                $button = \trans('theme::app.planbutton-' . $plan->title);
-                if ($button == 'theme::app.planbutton-' . $plan->title) {
-                    $button = \trans('app.planbutton', ['plan' => $plan->name]);
+        $plans = Plan::withEnvTenantContext()->orderBy('months')->orderByDesc('title')->get()
+            ->map(function ($plan) {
+                $button = self::trans("app.planbutton-{$plan->title}");
+                if (strpos($button, 'app.planbutton') !== false) {
+                    $button = self::trans('app.planbutton', ['plan' => $plan->name]);
                 }
 
-                $plans[] = [
+                return [
                     'title' => $plan->title,
                     'name' => $plan->name,
                     'button' => $button,
                     'description' => $plan->description,
-                    'mode' => $plan->mode ?: 'email',
+                    'mode' => $plan->mode ?: Plan::MODE_EMAIL,
                     'isDomain' => $plan->hasDomain(),
                 ];
-            });
+            })
+            ->all();
 
         return response()->json(['status' => 'success', 'plans' => $plans]);
     }
@@ -91,7 +90,7 @@
 
         $plan = $this->getPlan();
 
-        if ($plan->mode == 'token') {
+        if ($plan->mode == Plan::MODE_TOKEN) {
             $rules['token'] = ['required', 'string', new SignupToken()];
         } else {
             $rules['email'] = ['required', 'string', new SignupExternalEmail()];
@@ -106,7 +105,7 @@
 
         // Generate the verification code
         $code = SignupCode::create([
-                'email' => $plan->mode == 'token' ? $request->token : $request->email,
+                'email' => $plan->mode == Plan::MODE_TOKEN ? $request->token : $request->email,
                 'first_name' => $request->first_name,
                 'last_name' => $request->last_name,
                 'plan' => $plan->title,
@@ -119,7 +118,7 @@
             'mode' => $plan->mode ?: 'email',
         ];
 
-        if ($plan->mode == 'token') {
+        if ($plan->mode == Plan::MODE_TOKEN) {
             // Token verification, jump to the last step
             $has_domain = $plan->hasDomain();
 
@@ -221,13 +220,13 @@
     }
 
     /**
-     * Finishes the signup process by creating the user account.
+     * Validates the input to the final signup request.
      *
      * @param \Illuminate\Http\Request $request HTTP request
      *
      * @return \Illuminate\Http\JsonResponse JSON response
      */
-    public function signup(Request $request)
+    public function signupValidate(Request $request)
     {
         // Validate input
         $v = Validator::make(
@@ -244,14 +243,13 @@
             return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
         }
 
-
         $settings = [];
 
         // Plan parameter is required/allowed in mandate mode
         if (!empty($request->plan) && empty($request->code) && empty($request->invitation)) {
             $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
 
-            if (!$plan || $plan->mode != 'mandate') {
+            if (!$plan || $plan->mode != Plan::MODE_MANDATE) {
                 $msg = \trans('validation.exists', ['attribute' => 'plan']);
                 return response()->json(['status' => 'error', 'errors' => ['plan' => $msg]], 422);
             }
@@ -300,7 +298,7 @@
                 'last_name' => $code_data->last_name,
             ];
 
-            if ($plan->mode == 'token') {
+            if ($plan->mode == Plan::MODE_TOKEN) {
                 $settings['signup_token'] = $code_data->email;
             } else {
                 $settings['external_email'] = $code_data->email;
@@ -323,17 +321,46 @@
         }
 
         $is_domain = $plan->hasDomain();
-        $login = $request->login;
-        $domain_name = $request->domain;
 
         // Validate login
-        if ($errors = self::validateLogin($login, $domain_name, $is_domain)) {
+        if ($errors = self::validateLogin($request->login, $request->domain, $is_domain)) {
             return response()->json(['status' => 'error', 'errors' => $errors], 422);
         }
 
+        // Set some properties for signup() method
+        $request->settings = $settings;
+        $request->plan = $plan;
+        $request->discount = $discount ?? null;
+        $request->invitation = $invitation ?? null;
+
+        $result = [];
+
+        if ($plan->mode == Plan::MODE_MANDATE) {
+            $result = $this->mandateForPlan($plan, $request->discount);
+        }
+
+        return response()->json($result);
+    }
+
+    /**
+     * Finishes the signup process by creating the user account.
+     *
+     * @param \Illuminate\Http\Request $request HTTP request
+     *
+     * @return \Illuminate\Http\JsonResponse JSON response
+     */
+    public function signup(Request $request)
+    {
+        $v = $this->signupValidate($request);
+        if ($v->status() !== 200) {
+            return $v;
+        }
+
+        $is_domain = $request->plan->hasDomain();
+
         // We allow only ASCII, so we can safely lower-case the email address
-        $login = Str::lower($login);
-        $domain_name = Str::lower($domain_name);
+        $login = Str::lower($request->login);
+        $domain_name = Str::lower($request->domain);
         $domain = null;
 
         DB::beginTransaction();
@@ -353,22 +380,22 @@
                 'status' => User::STATUS_RESTRICTED,
         ]);
 
-        if (!empty($discount)) {
+        if ($request->discount) {
             $wallet = $user->wallets()->first();
-            $wallet->discount()->associate($discount);
+            $wallet->discount()->associate($request->discount);
             $wallet->save();
         }
 
-        $user->assignPlan($plan, $domain);
+        $user->assignPlan($request->plan, $domain);
 
         // Save the external email and plan in user settings
-        $user->setSettings($settings);
+        $user->setSettings($request->settings);
 
         // Update the invitation
-        if (!empty($invitation)) {
-            $invitation->status = SignupInvitation::STATUS_COMPLETED;
-            $invitation->user_id = $user->id;
-            $invitation->save();
+        if ($request->invitation) {
+            $request->invitation->status = SignupInvitation::STATUS_COMPLETED;
+            $request->invitation->user_id = $user->id;
+            $request->invitation->save();
         }
 
         // Soft-delete the verification code, and store some more info with it
@@ -384,15 +411,68 @@
 
         $response = AuthController::logonResponse($user, $request->password);
 
-        // Redirect the user to the specified page
-        // $data = $response->getData(true);
-        // $data['redirect'] = 'wallet';
-        // $response->setData($data);
+        if ($request->plan->mode == Plan::MODE_MANDATE) {
+            $data = $response->getData(true);
+            $data['checkout'] = $this->mandateForPlan($request->plan, $request->discount, $user);
+            $response->setData($data);
+        }
 
         return $response;
     }
 
     /**
+     * Collects some content to display to the user before redirect to a checkout page.
+     * Optionally creates a recurrent payment mandate for specified user/plan.
+     */
+    protected function mandateForPlan(Plan $plan, Discount $discount = null, User $user = null): array
+    {
+        $result = [];
+
+        $min = \App\Payment::MIN_AMOUNT;
+        $planCost = $plan->cost() * $plan->months;
+
+        if ($discount) {
+            $planCost -= ceil($planCost * (100 - $discount->discount) / 100);
+        }
+
+        if ($planCost > $min) {
+            $min = $planCost;
+        }
+
+        if ($user) {
+            $wallet = $user->wallets()->first();
+            $wallet->setSettings([
+                'mandate_amount' => sprintf('%.2f', round($min / 100, 2)),
+                'mandate_balance' => 0,
+            ]);
+
+            $mandate = [
+                'currency' => $wallet->currency,
+                'description' => \App\Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup',
+                'methodId' => PaymentProvider::METHOD_CREDITCARD,
+                'redirectUrl' => \App\Utils::serviceUrl('/payment/status', $user->tenant_id),
+            ];
+
+            $provider = PaymentProvider::factory($wallet);
+
+            $result = $provider->createMandate($wallet, $mandate);
+        }
+
+        $params = [
+            'cost' => \App\Utils::money($planCost, \config('app.currency')),
+            'period' => \trans($plan->months == 12 ? 'app.period-year' : 'app.period-month'),
+        ];
+
+        $content = '<b>' . self::trans('app.signup-account-tobecreated') . '</b><br><br>'
+            . self::trans('app.signup-account-summary', $params) . '<br><br>'
+            . self::trans('app.signup-account-mandate', $params);
+
+        $result['content'] = $content;
+
+        return $result;
+    }
+
+    /**
      * Returns plan for the signup process
      *
      * @returns \App\Plan Plan object selected for current signup process
diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/V4/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -140,6 +140,36 @@
     }
 
     /**
+     * Reset the auto-payment mandate, create a new payment for it.
+     *
+     * @param \Illuminate\Http\Request $request The API request.
+     *
+     * @return \Illuminate\Http\JsonResponse The response
+     */
+    public function mandateReset(Request $request)
+    {
+        $user = $this->guard()->user();
+
+        // TODO: Wallet selection
+        $wallet = $user->wallets()->first();
+
+        $mandate = [
+            'currency' => $wallet->currency,
+            'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup',
+            'methodId' => $request->methodId ?: PaymentProvider::METHOD_CREDITCARD,
+            'redirectUrl' => \App\Utils::serviceUrl('/payment/status', $user->tenant_id),
+        ];
+
+        $provider = PaymentProvider::factory($wallet);
+
+        $result = $provider->createMandate($wallet, $mandate);
+
+        $result['status'] = 'success';
+
+        return response()->json($result);
+    }
+
+    /**
      * Validate an auto-payment mandate request.
      *
      * @param \Illuminate\Http\Request $request The API request.
@@ -172,7 +202,7 @@
         $label = 'minamount';
 
         if (($plan = $wallet->plan()) && $plan->months >= 1) {
-            $planCost = (int) ceil($plan->cost() * $plan->months);
+            $planCost = $plan->cost() * $plan->months;
             if ($planCost > $min) {
                 $min = $planCost;
             }
@@ -191,6 +221,39 @@
     }
 
     /**
+     * Get status of the last payment.
+     *
+     * @return \Illuminate\Http\JsonResponse The response
+     */
+    public function paymentStatus()
+    {
+        $user = $this->guard()->user();
+        $wallet = $user->wallets()->first();
+
+        $payment = $wallet->payments()->orderBy('created_at', 'desc')->first();
+
+        if (empty($payment)) {
+            return $this->errorResponse(404);
+        }
+
+        $done = [Payment::STATUS_PAID, Payment::STATUS_CANCELED, Payment::STATUS_FAILED, Payment::STATUS_EXPIRED];
+
+        if (in_array($payment->status, $done)) {
+            $label = "app.payment-status-{$payment->status}";
+        } else {
+            $label = "app.payment-status-checking";
+        }
+
+        return response()->json([
+                'id' => $payment->id,
+                'status' => $payment->status,
+                'type' => $payment->type,
+                'statusMessage' => \trans($label),
+                'description' => $payment->description,
+        ]);
+    }
+
+    /**
      * Create a new payment.
      *
      * @param \Illuminate\Http\Request $request The API request.
@@ -386,7 +449,7 @@
 
         // If this is a multi-month plan, we calculate the expected amount to be payed.
         if (($plan = $wallet->plan()) && $plan->months >= 1) {
-            $planCost = (int) ceil(($plan->cost() * $plan->months) / 100);
+            $planCost = round($plan->cost() * $plan->months / 100, 2);
             if ($planCost > $mandate['minAmount']) {
                 $mandate['minAmount'] = $planCost;
             }
@@ -514,6 +577,9 @@
 
     /**
      * Calculates tax for the payment, fills the request with additional properties
+     *
+     * @param \App\Wallet $wallet  The wallet
+     * @param array       $request The request data with the payment amount
      */
     protected static function addTax(Wallet $wallet, array &$request): void
     {
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,6 +4,7 @@
 
 use App\Http\Controllers\RelationController;
 use App\Domain;
+use App\Plan;
 use App\Rules\Password;
 use App\Rules\UserEmailDomain;
 use App\Rules\UserEmailLocal;
@@ -203,7 +204,7 @@
             'enableUsers' => $isController,
             'enableWallets' => $isController,
             'enableWalletMandates' => $isController,
-            'enableWalletPayments' => $isController && (!$plan || $plan->mode != 'mandate'),
+            'enableWalletPayments' => $isController && (!$plan || $plan->mode != Plan::MODE_MANDATE),
             'enableCompanionapps' => $hasBeta,
         ];
 
@@ -356,7 +357,7 @@
         $wallet = $user->wallet();
 
         // IsLocked flag to lock the user to the Wallet page only
-        $response['isLocked'] = ($user->isRestricted() && ($plan = $wallet->plan()) && $plan->mode == 'mandate');
+        $response['isLocked'] = (!$user->isActive() && ($plan = $wallet->plan()) && $plan->mode == Plan::MODE_MANDATE);
 
         // Settings
         $response['settings'] = [];
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -81,4 +81,20 @@
     {
         return Auth::guard();
     }
+
+    /**
+     * A wrapper for \trans() with theme localization support.
+     *
+     * @param string $label  Localization label
+     * @param array  $params Translation parameters
+     */
+    public static function trans(string $label, array $params = []): string
+    {
+        $result = \trans("theme::{$label}", $params);
+        if ($result === "theme::{$label}") {
+            $result = \trans($label, $params);
+        }
+
+        return $result;
+    }
 }
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -102,7 +102,14 @@
             $user->status |= \App\User::STATUS_IMAP_READY;
         }
 
-        $user->status |= \App\User::STATUS_ACTIVE;
+        // Make user active in non-mandate mode only
+        if (!($wallet = $user->wallet())
+            || !($plan = $user->wallet()->plan())
+            || $plan->mode != \App\Plan::MODE_MANDATE
+        ) {
+            $user->status |= \App\User::STATUS_ACTIVE;
+        }
+
         $user->save();
     }
 }
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -12,6 +12,8 @@
  * @property int         $credit_amount     Amount of money in cents of system currency (wallet balance)
  * @property string      $description       Payment description
  * @property string      $id                Mollie's Payment ID
+ * @property string      $status            Payment status (Payment::STATUS_*)
+ * @property string      $type              Payment type (Payment::TYPE_*)
  * @property ?string     $vat_rate_id       VAT rate identifier
  * @property \App\Wallet $wallet            The wallet
  * @property string      $wallet_id         The ID of the wallet
@@ -126,9 +128,16 @@
             $this->wallet->setSetting('mandate_disabled', null);
         }
 
-        // Remove RESTRICTED flag from the wallet owner and all users in the wallet
-        if ($this->wallet->owner && $this->wallet->owner->isRestricted()) {
-            $this->wallet->owner->unrestrict(true);
+        if ($owner = $this->wallet->owner) {
+            // Remove RESTRICTED flag from the wallet owner and all users in the wallet
+            if ($owner->isRestricted()) {
+                $owner->unrestrict(true);
+            }
+            // Activate the inactive user
+            if (!$owner->isActive()) {
+                $owner->status |= User::STATUS_ACTIVE;
+                $owner->save();
+            }
         }
     }
 
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -20,7 +20,7 @@
  * @property int            $discount_rate
  * @property int            $free_months
  * @property string         $id
- * @property string         $mode           Plan signup mode (email|token)
+ * @property string         $mode           Plan signup mode (Plan::MODE_*)
  * @property string         $name
  * @property \App\Package[] $packages
  * @property datetime       $promo_from
@@ -34,6 +34,11 @@
     use HasTranslations;
     use UuidStrKeyTrait;
 
+    public const MODE_EMAIL = 'email';
+    public const MODE_TOKEN = 'token';
+    public const MODE_MANDATE = 'mandate';
+
+    /** @var bool Indicates if the model should be timestamped. */
     public $timestamps = false;
 
     /** @var array<int, string> The attributes that are mass assignable */
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -54,6 +54,7 @@
      *                             - currency: The operation currency
      *                             - description: Operation desc.
      *                             - methodId: Payment method
+     *                             - redirectUrl: The location to goto after checkout
      *
      * @return array Provider payment data:
      *               - id: Operation identifier
@@ -80,7 +81,7 @@
             'sequenceType' => 'first',
             'description' => $payment['description'],
             'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
-            'redirectUrl' => self::redirectUrl(),
+            'redirectUrl' => $payment['redirectUrl'] ?? self::redirectUrl(),
             'locale' => 'en_US',
             'method' => $payment['methodId']
         ];
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -59,6 +59,7 @@
      *                             - amount: Value in cents (not used)
      *                             - currency: The operation currency
      *                             - description: Operation desc.
+     *                             - redirectUrl: The location to goto after checkout
      *
      * @return array Provider payment/session data:
      *               - id: Session identifier
@@ -70,8 +71,8 @@
 
         $request = [
             'customer' => $customer_id,
-            'cancel_url' => self::redirectUrl(), // required
-            'success_url' => self::redirectUrl(), // required
+            'cancel_url' => $payment['redirectUrl'] ?? self::redirectUrl(), // required
+            'success_url' => $payment['redirectUrl'] ?? self::redirectUrl(), // required
             'payment_method_types' => ['card'], // required
             'locale' => 'en',
             'mode' => 'setup',
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -89,6 +89,7 @@
      *                             - currency: The operation currency
      *                             - description: Operation desc.
      *                             - methodId: Payment method
+     *                             - redirectUrl: The location to goto after checkout
      *
      * @return array Provider payment data:
      *               - id: Operation identifier
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -576,4 +576,25 @@
 
         return floatval($rates[$targetCurrency]);
     }
+
+    /**
+     * A helper to display human-readable amount of money using
+     * for specified currency and locale.
+     *
+     * @param int    $amount   Amount of money (in cents)
+     * @param string $currency Currency code
+     * @param string $locale   Output locale
+     *
+     * @return string String representation, e.g. "9.99 CHF"
+     */
+    public static function money(int $amount, $currency, $locale = 'de_DE'): string
+    {
+        $amount = round($amount / 100, 2);
+
+        $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+        $result = $nf->formatCurrency($amount, $currency);
+
+        // Replace non-breaking space
+        return str_replace("\xC2\xA0", " ", $result);
+    }
 }
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -462,12 +462,7 @@
      */
     public function money(int $amount, $locale = 'de_DE')
     {
-        $amount = round($amount / 100, 2);
-
-        $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
-        $result = $nf->formatCurrency($amount, $this->currency);
-        // Replace non-breaking space
-        return str_replace("\xC2\xA0", " ", $result);
+        return \App\Utils::money($amount, $this->currency, $locale);
     }
 
     /**
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -33,9 +33,9 @@
         return
     }
 
-    if (routerState.isLocked && to.meta.requiresAuth && !['login', 'wallet'].includes(to.name)) {
-        // redirect to the wallet page
-        next({ name: 'wallet' })
+    if (routerState.isLocked && to.meta.requiresAuth && !['login', 'payment-status'].includes(to.name)) {
+        // redirect to the payment-status page
+        next({ name: 'payment-status' })
         return
     }
 
@@ -149,9 +149,9 @@
 
             if (dashboard !== false) {
                 this.$router.push(routerState.afterLogin || { name: response.redirect || 'dashboard' })
-            } else if (routerState.isLocked && this.$route.name != 'wallet' && this.$route.meta.requiresAuth) {
+            } else if (routerState.isLocked && this.$route.meta.requiresAuth && this.$route.name != 'payment-status') {
                 // Always redirect locked user, here we can be after router's beforeEach handler
-                this.$router.push({ name: 'wallet' })
+                this.$router.push({ name: 'payment-status' })
             }
 
             routerState.afterLogin = null
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -17,6 +17,7 @@
 const DomainListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Domain/List')
 const FileInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/Info')
 const FileListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/List')
+const PaymentStatusComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Payment/Status')
 const ResourceInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/Info')
 const ResourceListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/List')
 const RoomInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/Info')
@@ -108,6 +109,12 @@
         component: PasswordResetComponent
     },
     {
+        path: '/payment/status',
+        name: 'payment-status',
+        component: PaymentStatusComponent,
+        meta: { requiresAuth: true }
+    },
+    {
         path: '/profile',
         name: 'profile',
         component: UserProfileComponent,
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -79,6 +79,15 @@
     'file-permissions-update-success' => 'File permissions updated successfully.',
     'file-permissions-delete-success' => 'File permissions deleted successfully.',
 
+    'payment-status-paid' => 'The payment has been completed successfully.',
+    'payment-status-canceled' => 'The payment has been canceled.',
+    'payment-status-failed' => 'The payment failed.',
+    'payment-status-expired' => 'The payment expired.',
+    'payment-status-checking' => "The payment hasn't been completed yet. Checking the status...",
+
+    'period-year' => 'year',
+    'period-month' => 'month',
+
     'resource-update-success' => 'Resource updated successfully.',
     'resource-create-success' => 'Resource created successfully.',
     'resource-delete-success' => 'Resource deleted successfully.',
@@ -112,6 +121,10 @@
     'search-foundxshared-folders' => ':x shared folders have been found.',
     'search-foundxusers' => ':x user accounts have been found.',
 
+    'signup-account-tobecreated' => 'The account is about to be created!',
+    'signup-account-mandate' => 'Now it is required to provide your credit card details.'
+        . ' This way you agree to charge you with an appropriate amount of money according to the plan you signed up for.',
+    'signup-account-summary' => 'You signed up for an account with a base cost of :cost per :period.',
     'signup-invitations-created' => 'The invitation has been created.|:count invitations has been created.',
     'signup-invitations-csv-empty' => 'Failed to find any valid email addresses in the uploaded file.',
     'signup-invitations-csv-invalid-email' => 'Found an invalid email address (:email) on line :line.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -35,6 +35,7 @@
         'signup' => "Sign Up",
         'submit' => "Submit",
         'suspend' => "Suspend",
+        'tryagain' => "Try again",
         'unsuspend' => "Unsuspend",
         'verify' => "Verify",
     ],
diff --git a/src/resources/vue/Payment/Status.vue b/src/resources/vue/Payment/Status.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Payment/Status.vue
@@ -0,0 +1,64 @@
+<template>
+    <div class="container">
+        <p v-if="$root.authInfo.isLocked" id="lock-alert" class="alert alert-warning">
+            {{ $t('wallet.locked-text') }}
+        </p>
+        <div class="card">
+            <div class="card-body">
+                <div class="card-text" v-html="payment.statusMessage"></div>
+                <div class="mt-4">
+                    <btn v-if="payment.tryagain" @click="tryAgain" class="btn-primary">{{ $t('btn.tryagain') }}</btn>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        data() {
+            return {
+                payment: {}
+            }
+        },
+        mounted() {
+            this.paymentStatus(true)
+        },
+        beforeDestroy() {
+            clearTimeout(this.timeout)
+        },
+        methods: {
+            paymentStatus(loader) {
+                axios.get('/api/v4/payments/status', { loader })
+                    .then(response => {
+                        this.payment = response.data
+                        this.payment.tryagain = this.payment.type == 'mandate' && this.payment.status != 'paid'
+
+                        if (this.payment.status == 'paid' && this.$root.authInfo.isLocked) {
+                            // unlock, and redirect to the Dashboard
+                            this.timeout = setTimeout(() => this.$root.unlock(), 5000)
+                        } else if (['open', 'pending', 'authorized'].includes(this.payment.status)) {
+                            // wait some time and check again
+                            this.timeout = setTimeout(() => this.paymentStatus(false), 5000)
+                        }
+                    })
+                    .catch(error => {
+                        this.$root.errorHandler(error)
+                    })
+            },
+            tryAgain() {
+                // Create the first payment and goto to the checkout page, again
+                axios.post('/api/v4/payments/mandate/reset')
+                    .then(response => {
+                        clearTimeout(this.timeout)
+                        // TODO: We have this code in a few places now, de-duplicate!
+                        if (response.data.redirectUrl) {
+                            location.href = response.data.redirectUrl
+                        } else if (response.data.id) {
+                            // TODO: this.stripeCheckout(response.data)
+                        }
+                    })
+            }
+        }
+    }
+</script>
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -100,6 +100,16 @@
                 </form>
             </div>
         </div>
+
+        <div class="card d-none" id="step4">
+            <div class="card-body">
+                <div class="card-text mb-4" v-html="checkout.content"></div>
+                <form>
+                    <btn class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
+                    <btn class="btn-primary" @click="submitStep4">{{ $t('btn.continue') }}</btn>
+                </form>
+            </div>
+        </div>
     </div>
 </template>
 
@@ -119,6 +129,7 @@
         },
         data() {
             return {
+                checkout: {},
                 email: '',
                 first_name: '',
                 last_name: '',
@@ -296,23 +307,34 @@
             submitStep3() {
                 this.$root.clearFormValidation($('#step3 form'))
 
-                let post = {
-                    ...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
-                    ...this.pass
-                }
+                const post = this.lastStepPostData()
 
-                if (this.invitation) {
-                    post.invitation = this.invitation.id
-                    post.first_name = this.first_name
-                    post.last_name = this.last_name
+                if (this.mode == 'mandate') {
+                    axios.post('/api/auth/signup/validate', post).then(response => {
+                        this.checkout = response.data
+                        this.displayForm(4)
+                    })
                 } else {
-                    post.code = this.code
-                    post.short_code = this.short_code
+                    axios.post('/api/auth/signup', post).then(response => {
+                        // auto-login and goto dashboard
+                        this.$root.loginUser(response.data)
+                    })
                 }
+            },
+            submitStep4() {
+                const post = this.lastStepPostData()
 
                 axios.post('/api/auth/signup', post).then(response => {
-                    // auto-login and goto dashboard
-                    this.$root.loginUser(response.data)
+                    // auto-login and goto to the payment checkout
+                    this.$root.loginUser(response.data, false)
+
+                    let checkout = response.data.checkout
+
+                    if (checkout.redirectUrl) {
+                        location.href = checkout.redirectUrl
+                    } else if (checkout.id) {
+                        // TODO: this.stripeCheckout(checkout)
+                    }
                 })
             },
             // Moves the user a step back in registration form
@@ -328,7 +350,7 @@
                     step = 1
                 }
 
-                if (this.mode == 'mandate') {
+                if (this.mode == 'mandate' && step < 3) {
                     step = 0
                 }
 
@@ -340,7 +362,7 @@
                 }
             },
             displayForm(step, focus) {
-                [0, 1, 2, 3].filter(value => value != step).forEach(value => {
+                [0, 1, 2, 3, 4].filter(value => value != step).forEach(value => {
                     $('#step' + value).addClass('d-none')
                 })
 
@@ -354,6 +376,23 @@
                     $('#step' + step).find('input').first().focus()
                 }
             },
+            lastStepPostData() {
+                let post = {
+                    ...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
+                    ...this.pass
+                }
+
+                if (this.invitation) {
+                    post.invitation = this.invitation.id
+                    post.first_name = this.first_name
+                    post.last_name = this.last_name
+                } else {
+                    post.code = this.code
+                    post.short_code = this.short_code
+                }
+
+                return post
+            },
             setDomain(response) {
                 if (response.domains) {
                     this.domains = response.domains
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -1,8 +1,5 @@
 <template>
     <div class="container" dusk="wallet-component">
-        <p v-if="$root.authInfo.isLocked" id="lock-alert" class="alert alert-warning">
-            {{ $t('wallet.locked-text') }}
-        </p>
         <div v-if="wallet.id" id="wallet" class="card">
             <div class="card-body">
                 <div class="card-title">{{ $t('wallet.title') }} <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
@@ -221,9 +218,6 @@
                 return tabs
             }
         },
-        beforeDestroyed() {
-            clearTimeout(this.refreshRequest)
-        },
         mounted() {
             $('#wallet button').focus()
 
@@ -255,32 +249,21 @@
             this.$refs.tabs.clickHandler('payments', () => { this.loadPayments = true })
         },
         methods: {
-            loadMandate(refresh) {
+            loadMandate() {
                 const loader = '#mandate-form'
 
                 this.$root.stopLoading(loader)
 
-                if (!this.mandate.id || this.mandate.isPending || refresh) {
-                    axios.get('/api/v4/payments/mandate', refresh ? {} : { loader })
-                        .then(response => {
-                            this.mandate = response.data
-
-                            if (this.mandate.minAmount) {
-                                if (this.mandate.minAmount > this.mandate.amount) {
-                                    this.mandate.amount = this.mandate.minAmount
-                                }
-                            }
+                axios.get('/api/v4/payments/mandate', { loader })
+                    .then(response => {
+                        this.mandate = response.data
 
-                            if (this.$root.authInfo.isLocked) {
-                                if (this.mandate.isValid) {
-                                    this.$root.unlock()
-                                } else {
-                                    clearTimeout(this.refreshRequest)
-                                    this.refreshRequest = setTimeout(() => { this.loadMandate(true) }, 10 * 1000)
-                                }
+                        if (this.mandate.minAmount) {
+                            if (this.mandate.minAmount > this.mandate.amount) {
+                                this.mandate.amount = this.mandate.minAmount
                             }
-                        })
-                }
+                        }
+                    })
             },
             selectPaymentMethod(method) {
                 this.formLock = false
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -53,6 +53,7 @@
         Route::post('signup/init', [API\SignupController::class, 'init']);
         Route::get('signup/invitations/{id}', [API\SignupController::class, 'invitation']);
         Route::get('signup/plans', [API\SignupController::class, 'plans']);
+        Route::post('signup/validate', [API\SignupController::class, 'signupValidate']);
         Route::post('signup/verify', [API\SignupController::class, 'verify']);
         Route::post('signup', [API\SignupController::class, 'signup']);
     }
@@ -151,9 +152,11 @@
         Route::post('payments/mandate', [API\V4\PaymentsController::class, 'mandateCreate']);
         Route::put('payments/mandate', [API\V4\PaymentsController::class, 'mandateUpdate']);
         Route::delete('payments/mandate', [API\V4\PaymentsController::class, 'mandateDelete']);
+        Route::post('payments/mandate/reset', [API\V4\PaymentsController::class, 'mandateReset']);
         Route::get('payments/methods', [API\V4\PaymentsController::class, 'paymentMethods']);
         Route::get('payments/pending', [API\V4\PaymentsController::class, 'payments']);
         Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']);
+        Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']);
 
         Route::post('support/request', [API\V4\SupportController::class, 'request'])
             ->withoutMiddleware(['auth:api', 'scope:api'])
diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/PaymentStatus.php
copy from src/tests/Browser/Pages/Signup.php
copy to src/tests/Browser/Pages/PaymentStatus.php
--- a/src/tests/Browser/Pages/Signup.php
+++ b/src/tests/Browser/Pages/PaymentStatus.php
@@ -4,7 +4,7 @@
 
 use Laravel\Dusk\Page;
 
-class Signup extends Page
+class PaymentStatus extends Page
 {
     /**
      * Get the URL for the page.
@@ -13,7 +13,7 @@
      */
     public function url(): string
     {
-        return '/signup';
+        return '/payment/status';
     }
 
     /**
@@ -25,12 +25,8 @@
      */
     public function assert($browser)
     {
-        $browser->assertPathIs('/signup')
-            ->waitUntilMissing('.app-loader')
-            ->assertPresent('@step0')
-            ->assertPresent('@step1')
-            ->assertPresent('@step2')
-            ->assertPresent('@step3');
+        $browser->waitForLocation($this->url())
+            ->waitUntilMissing('@app .app-loader');
     }
 
     /**
@@ -42,10 +38,9 @@
     {
         return [
             '@app' => '#app',
-            '@step0' => '#step0',
-            '@step1' => '#step1',
-            '@step2' => '#step2',
-            '@step3' => '#step3',
+            '@content' => '.card .card-text',
+            '@lock-alert' => '#lock-alert',
+            '@button' => '.card button.btn-primary',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Signup.php b/src/tests/Browser/Pages/Signup.php
--- a/src/tests/Browser/Pages/Signup.php
+++ b/src/tests/Browser/Pages/Signup.php
@@ -46,6 +46,7 @@
             '@step1' => '#step1',
             '@step2' => '#step2',
             '@step3' => '#step3',
+            '@step4' => '#step4',
         ];
     }
 }
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -13,8 +13,9 @@
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\PaymentMollie;
+use Tests\Browser\Pages\PaymentStatus;
 use Tests\Browser\Pages\Signup;
-use Tests\Browser\Pages\Wallet;
 use Tests\TestCaseDusk;
 use Illuminate\Foundation\Testing\DatabaseMigrations;
 
@@ -31,7 +32,7 @@
         $this->deleteTestUser('admin@user-domain-signup.com');
         $this->deleteTestDomain('user-domain-signup.com');
 
-        Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
+        Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
     }
 
     /**
@@ -44,7 +45,7 @@
         $this->deleteTestDomain('user-domain-signup.com');
         SignupInvitation::truncate();
 
-        Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']);
+        Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
 
         @unlink(storage_path('signup-tokens.txt'));
 
@@ -521,17 +522,22 @@
     }
 
     /**
-     * Test signup with a mandate plan, also the wallet lock
+     * Test signup with a mandate plan, also the UI lock
+     *
+     * @group mollie
      */
     public function testSignupMandate(): void
     {
         // Test the individual plan
         $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
-        $plan->mode = 'mandate';
+        $plan->mode = Plan::MODE_MANDATE;
         $plan->save();
 
         $this->browse(function (Browser $browser) {
+            $config = ['paymentProvider' => 'mollie'];
             $browser->visit(new Signup())
+                // Force Mollie
+                ->execScript(sprintf('Object.assign(window.config, %s)', \json_encode($config)))
                 ->waitFor('@step0 .plan-individual button')
                 ->click('@step0 .plan-individual button')
                 // Test Back button
@@ -555,30 +561,57 @@
                         ->type('#signup_password_confirmation', '12345678')
                         ->click('[type=submit]');
                 })
-                ->waitUntilMissing('@step3')
-                ->on(new Wallet())
-                ->assertSeeIn('#lock-alert', "The account is locked")
-                ->within(new Menu(), function ($browser) {
-                    $browser->clickMenuItem('logout');
-                });
+                ->whenAvailable('@step4', function ($browser) {
+                    $browser->assertSeeIn('.card-text', 'The account is about to be created!')
+                        ->assertSeeIn('.card-text', 'You signed up for an account')
+                        ->assertSeeIn('button.btn-primary', 'Continue')
+                        ->assertSeeIn('button.btn-secondary', 'Back')
+                        ->click('button.btn-secondary');
+                })
+                ->whenAvailable('@step3', function ($browser) {
+                    $browser->assertValue('#signup_login', 'signuptestdusk')
+                        ->click('[type=submit]');
+                })
+                ->whenAvailable('@step4', function ($browser) {
+                    $browser->click('button.btn-primary');
+                })
+                ->on(new PaymentMollie())
+                ->assertSeeIn('@title', 'Auto-Payment Setup')
+                ->assertMissing('@amount')
+                ->submitPayment('open')
+                ->on(new PaymentStatus())
+                ->assertSeeIn('@lock-alert', 'The account is locked')
+                ->assertSeeIn('@content', 'Checking the status...')
+                ->assertSeeIn('@button', 'Try again');
         });
 
         $user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
         $this->assertSame($plan->id, $user->getSetting('plan_id'));
+        $this->assertFalse($user->isActive());
 
-        // Login again and see that the account is still locked
+        // Refresh and see that the account is still locked
         $this->browse(function (Browser $browser) use ($user) {
-            $browser->on(new Home())
-                ->submitLogon($user->email, '12345678', false)
-                ->waitForLocation('/wallet')
-                ->on(new Wallet())
-                ->assertSeeIn('#lock-alert', "The account is locked")
+            $browser->visit('/dashboard')
+                ->on(new PaymentStatus())
+                ->assertSeeIn('@lock-alert', 'The account is locked')
+                ->assertSeeIn('@content', 'Checking the status...');
+
+            // Mark the payment paid, and activate the user in background,
+            // expect unlock and redirect to the dashboard
+            // TODO: Move this to a separate tests file for PaymentStatus page
+            $payment = $user->wallets()->first()->payments()->first();
+            $payment->credit('Test');
+            $payment->status = \App\Payment::STATUS_PAID;
+            $payment->save();
+            $this->assertTrue($user->fresh()->isActive());
+
+            $browser->waitForLocation('/dashboard', 10)
                 ->within(new Menu(), function ($browser) {
                     $browser->clickMenuItem('logout');
                 });
-
-            // TODO: Test automatic UI unlock after creating a valid auto-payment mandate
         });
+
+        // TODO: Test the 'Try again' button on /payment/status page
     }
 
     /**
@@ -587,7 +620,7 @@
     public function testSignupToken(): void
     {
         // Test the individual plan
-        Plan::where('title', 'individual')->update(['mode' => 'token']);
+        Plan::where('title', 'individual')->update(['mode' => Plan::MODE_TOKEN]);
 
         // Register some valid tokens
         $tokens = ['1234567890', 'abcdefghijk'];
@@ -640,7 +673,7 @@
         $this->assertSame(null, $user->getSetting('external_email'));
 
         // Test the group plan
-        Plan::where('title', 'group')->update(['mode' => 'token']);
+        Plan::where('title', 'group')->update(['mode' => Plan::MODE_TOKEN]);
 
         $this->browse(function (Browser $browser) use ($tokens) {
             $browser->visit(new Signup())
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieTest.php
@@ -371,8 +371,8 @@
 
         $json = $response->json();
 
-        $this->assertSame((int) ceil(Payment::MIN_AMOUNT / 100), $json['amount']);
-        $this->assertSame((int) ceil(($plan->cost() * $plan->months) / 100), $json['minAmount']);
+        $this->assertEquals(round(Payment::MIN_AMOUNT / 100, 2), $json['amount']);
+        $this->assertEquals(round($plan->cost() * $plan->months / 100, 2), $json['minAmount']);
 
         // TODO: Test more cases
         // TODO: Test user unrestricting if mandate is valid
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -803,7 +803,7 @@
                 'free_months' => 1,
                 'discount_qty' => 0,
                 'discount_rate' => 0,
-                'mode' => 'mandate',
+                'mode' => Plan::MODE_MANDATE,
         ]);
 
         $packages = [
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -47,8 +47,7 @@
         $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
         $wallet->save();
         $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
-        $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY;
-        $user->status &= ~User::STATUS_RESTRICTED;
+        $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
         $user->save();
         Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
         $user->setSettings(['plan_id' => null]);
@@ -81,8 +80,7 @@
         $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete();
         $wallet->save();
         $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete();
-        $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY;
-        $user->status &= ~User::STATUS_RESTRICTED;
+        $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY | User::STATUS_ACTIVE;
         $user->save();
         Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']);
         $user->setSettings(['plan_id' => null]);
@@ -1411,7 +1409,7 @@
 
         // Ned is John's wallet controller
         $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
-        $plan->mode = 'mandate';
+        $plan->mode = Plan::MODE_MANDATE;
         $plan->save();
         $wallet->owner->setSettings(['plan_id' => $plan->id]);
         $ned = $this->getTestUser('ned@kolab.org');
@@ -1472,7 +1470,8 @@
         $this->assertFalse($result['isLocked']);
 
         // Test locked user
-        $john->restrict();
+        $john->status &= ~User::STATUS_ACTIVE;
+        $john->save();
         $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]);
 
         $this->assertTrue($result['isLocked']);
diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php
--- a/src/tests/Unit/UtilsTest.php
+++ b/src/tests/Unit/UtilsTest.php
@@ -59,6 +59,18 @@
     }
 
     /**
+     * Test for Utils::money()
+     */
+    public function testMoney(): void
+    {
+        $this->assertSame('-0,01 CHF', Utils::money(-1, 'CHF'));
+        $this->assertSame('0,00 CHF', Utils::money(0, 'CHF'));
+        $this->assertSame('1,11 €', Utils::money(111, 'EUR'));
+        $this->assertSame('1,00 CHF', Utils::money(100, 'CHF'));
+        $this->assertSame('€0.00', Utils::money(0, 'EUR', 'en_US'));
+    }
+
+    /**
      * Test for Utils::normalizeAddress()
      */
     public function testNormalizeAddress(): void