diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index cf9a968d56ba..658784a30311 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -29,13 +29,20 @@ class SendRenewalInvoices extends Command $this->info(date('Y-m-d').' Running SendRenewalInvoices...'); $today = new DateTime(); + // get all accounts with pro plans expiring in 10 days $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355')->get(); $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { $client = $this->accountRepo->getNinjaClient($account); $invitation = $this->accountRepo->createNinjaInvoice($client); - $this->mailer->sendInvoice($invitation->invoice); + + // set the due date to 10 days from now + $invoice = $invitation->invoice; + $invoice->due_date = date('Y-m-d', strtotime('+ 10 days')); + $invoice->save(); + + $this->mailer->sendInvoice($invoice); } $this->info('Done'); diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index f6990e28450e..1eb38bcbdc8a 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -43,6 +43,7 @@ use App\Ninja\Mailers\ContactMailer; use App\Events\UserSignedUp; use App\Events\UserLoggedIn; use App\Events\UserSettingsChanged; +use App\Services\AuthService; class AccountController extends BaseController { @@ -110,7 +111,6 @@ class AccountController extends BaseController } Auth::login($user, true); - event(new UserSignedUp()); event(new UserLoggedIn()); $redirectTo = Input::get('redirect_to') ?: 'invoices/create'; @@ -121,13 +121,6 @@ class AccountController extends BaseController { $invitation = $this->accountRepo->enableProPlan(); - /* - if ($invoice) - { - $this->contactMailer->sendInvoice($invoice); - } - */ - return $invitation->invitation_key; } @@ -153,7 +146,12 @@ class AccountController extends BaseController public function showSection($section = ACCOUNT_DETAILS, $subSection = false) { if ($section == ACCOUNT_DETAILS) { - $primaryUser = Auth::user()->account->users()->orderBy('id')->first(); + + $oauthLoginUrls = []; + foreach (AuthService::$providers as $provider) { + $oauthLoginUrls[] = ['label' => $provider, 'url' => '/auth/' . strtolower($provider)]; + } + $data = [ 'account' => Account::with('users')->findOrFail(Auth::user()->account_id), 'countries' => Cache::get('countries'), @@ -164,9 +162,10 @@ class AccountController extends BaseController 'datetimeFormats' => Cache::get('datetimeFormats'), 'currencies' => Cache::get('currencies'), 'languages' => Cache::get('languages'), - 'showUser' => Auth::user()->id === $primaryUser->id, 'title' => trans('texts.company_details'), - 'primaryUser' => $primaryUser, + 'user' => Auth::user(), + 'oauthProviderName' => AuthService::getProviderName(Auth::user()->oauth_provider_id), + 'oauthLoginUrls' => $oauthLoginUrls, ]; return View::make('accounts.details', $data); @@ -402,6 +401,8 @@ class AccountController extends BaseController $account->custom_invoice_label2 = trim(Input::get('custom_invoice_label2')); $account->custom_invoice_taxes1 = Input::get('custom_invoice_taxes1') ? true : false; $account->custom_invoice_taxes2 = Input::get('custom_invoice_taxes2') ? true : false; + $account->custom_invoice_text_label1 = trim(Input::get('custom_invoice_text_label1')); + $account->custom_invoice_text_label2 = trim(Input::get('custom_invoice_text_label2')); $account->invoice_number_prefix = Input::get('invoice_number_prefix'); $account->invoice_number_counter = Input::get('invoice_number_counter'); @@ -722,23 +723,21 @@ class AccountController extends BaseController $account->military_time = Input::get('military_time') ? true : false; $account->save(); - if (Auth::user()->id === $user->id) { - $user = Auth::user(); - $user->first_name = trim(Input::get('first_name')); - $user->last_name = trim(Input::get('last_name')); - $user->username = trim(Input::get('email')); - $user->email = trim(strtolower(Input::get('email'))); - $user->phone = trim(Input::get('phone')); - if (Utils::isNinja()) { - if (Input::get('referral_code')) { - $user->referral_code = $this->accountRepo->getReferralCode(); - } + $user = Auth::user(); + $user->first_name = trim(Input::get('first_name')); + $user->last_name = trim(Input::get('last_name')); + $user->username = trim(Input::get('email')); + $user->email = trim(strtolower(Input::get('email'))); + $user->phone = trim(Input::get('phone')); + if (Utils::isNinja()) { + if (Input::get('referral_code') && !$user->referral_code) { + $user->referral_code = $this->accountRepo->getReferralCode(); } - if (Utils::isNinjaDev()) { - $user->dark_mode = Input::get('dark_mode') ? true : false; - } - $user->save(); } + if (Utils::isNinjaDev()) { + $user->dark_mode = Input::get('dark_mode') ? true : false; + } + $user->save(); /* Logo image file */ if ($file = Input::file('logo')) { @@ -816,24 +815,14 @@ class AccountController extends BaseController $user->username = $user->email; $user->password = bcrypt(trim(Input::get('new_password'))); $user->registered = true; - $user->save(); - - if (Utils::isNinjaProd()) { - $this->userMailer->sendConfirmation($user); - } - - $activities = Activity::scope()->get(); - foreach ($activities as $activity) { - $activity->message = str_replace('Guest', $user->getFullName(), $activity->message); - $activity->save(); - } + $user->save(); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); } - Session::set(SESSION_COUNTER, -1); - + event(new UserSignedUp()); + return "{$user->first_name} {$user->last_name}"; } diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 21becbc7e067..5ced7948658e 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -20,18 +20,21 @@ use App\Models\Industry; use App\Ninja\Mailers\Mailer; use App\Ninja\Repositories\AccountRepository; use App\Events\UserSettingsChanged; +use App\Services\EmailService; class AppController extends BaseController { protected $accountRepo; protected $mailer; + protected $emailService; - public function __construct(AccountRepository $accountRepo, Mailer $mailer) + public function __construct(AccountRepository $accountRepo, Mailer $mailer, EmailService $emailService) { parent::__construct(); $this->accountRepo = $accountRepo; $this->mailer = $mailer; + $this->emailService = $emailService; } public function showSetup() @@ -195,4 +198,19 @@ class AppController extends BaseController return Redirect::to('/'); } + + public function emailBounced() + { + $messageId = Input::get('MessageID'); + $error = Input::get('Name') . ': ' . Input::get('Description'); + return $this->emailService->markBounced($messageId, $error) ? RESULT_SUCCESS : RESULT_FAILURE; + } + + public function emailOpened() + { + $messageId = Input::get('MessageID'); + return $this->emailService->markOpened($messageId) ? RESULT_SUCCESS : RESULT_FAILURE; + + return RESULT_SUCCESS; + } } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index a5897ac6879d..58891afe9763 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -9,6 +9,7 @@ use App\Models\User; use App\Events\UserLoggedIn; use App\Http\Controllers\Controller; use App\Ninja\Repositories\AccountRepository; +use App\Services\AuthService; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Registrar; use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; @@ -30,6 +31,7 @@ class AuthController extends Controller { protected $loginPath = '/login'; protected $redirectTo = '/dashboard'; + protected $authService; protected $accountRepo; /** @@ -39,15 +41,29 @@ class AuthController extends Controller { * @param \Illuminate\Contracts\Auth\Registrar $registrar * @return void */ - public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo) + public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo, AuthService $authService) { $this->auth = $auth; $this->registrar = $registrar; $this->accountRepo = $repo; + $this->authService = $authService; //$this->middleware('guest', ['except' => 'getLogout']); } + public function authLogin($provider, Request $request) + { + return $this->authService->execute($provider, $request->has('code')); + } + + public function authUnlink() + { + $this->accountRepo->unlinkUserFromOauth(Auth::user()); + + Session::flash('message', trans('texts.updated_settings')); + return redirect()->to('/company/details'); + } + public function getLoginWrapper() { if (!Utils::isNinja() && !User::count()) { @@ -63,7 +79,7 @@ class AuthController extends Controller { $user = User::where('email', '=', $request->input('email'))->first(); if ($user && $user->failed_logins >= 3) { - Session::flash('error', 'These credentials do not match our records.'); + Session::flash('error', trans('texts.invalid_credentials')); return redirect()->to('login'); } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 60c1e7578cd0..948a21532f0e 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -358,20 +358,26 @@ class InvoiceController extends BaseController $data['formIsChanged'] = true; } - // Set the invitation link on the client's contacts + // Set the invitation data on the client's contacts if (!$clone) { $clients = $data['clients']; foreach ($clients as $client) { - if ($client->id == $invoice->client->id) { - foreach ($invoice->invitations as $invitation) { - foreach ($client->contacts as $contact) { - if ($invitation->contact_id == $contact->id) { - $contact->invitation_link = $invitation->getLink(); - } + if ($client->id != $invoice->client->id) { + continue; + } + + foreach ($invoice->invitations as $invitation) { + foreach ($client->contacts as $contact) { + if ($invitation->contact_id == $contact->id) { + $contact->email_error = $invitation->email_error; + $contact->invitation_link = $invitation->getLink(); + $contact->invitation_viewed = $invitation->viewed_date; + $contact->invitation_status = $contact->email_error ? false : $invitation->getStatus(); } } - break; } + + break; } } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 33f62d417d81..903d6f028aab 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -14,6 +14,8 @@ class VerifyCsrfToken extends BaseVerifier { 'api/v1/tasks', 'api/v1/email_invoice', 'api/v1/hooks', + 'hook/email_opened', + 'hook/email_bounced', ]; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index 9f1e569fb768..2ec246b7ce42 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -63,6 +63,10 @@ Route::post('signup/validate', 'AccountController@checkEmail'); Route::post('signup/submit', 'AccountController@submitSignup'); Route::get('auth/{provider}', 'Auth\AuthController@authLogin'); +Route::get('auth_unlink', 'Auth\AuthController@authUnlink'); + +Route::post('hook/email_bounced', 'AppController@emailBounced'); +Route::post('hook/email_opened', 'AppController@emailOpened'); // Laravel auth routes @@ -436,7 +440,7 @@ if (!defined('CONTACT_EMAIL')) { define('SOCIAL_GOOGLE', 'Google'); define('SOCIAL_FACEBOOK', 'Facebook'); - define('SOCIAL_GITHUB', 'Github'); + define('SOCIAL_GITHUB', 'GitHub'); define('SOCIAL_LINKEDIN', 'LinkedIn'); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 60548af6ca7c..4c50b1059955 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -672,31 +672,6 @@ class Utils fwrite($output, "\n"); } - public static function stringToObjectResolution($baseObject, $rawPath) - { - $val = ''; - - if (!is_object($baseObject)) { - return $val; - } - - $path = preg_split('/->/', $rawPath); - $node = $baseObject; - - while (($prop = array_shift($path)) !== null) { - if (property_exists($node, $prop)) { - $val = $node->$prop; - $node = $node->$prop; - } else if (is_object($node) && isset($node->$prop)) { - $node = $node->{$prop}; - } else if ( method_exists($node, $prop)) { - $val = call_user_func(array($node, $prop)); - } - } - - return $val; - } - public static function getFirst($values) { if (is_array($values)) { return count($values) ? $values[0] : false; @@ -753,4 +728,12 @@ class Utils } return $domain; } + + public static function splitName($name) { + $name = trim($name); + $lastName = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); + $firstName = trim( preg_replace('#'.$lastName.'#', '', $name ) ); + return array($firstName, $lastName); + } + } diff --git a/app/Listeners/HandleUserSignedUp.php b/app/Listeners/HandleUserSignedUp.php index 8d45d3e5a62a..5bc4eab28d97 100644 --- a/app/Listeners/HandleUserSignedUp.php +++ b/app/Listeners/HandleUserSignedUp.php @@ -1,37 +1,52 @@ -accountRepo = $accountRepo; + $this->userMailer = $userMailer; } - /** - * Handle the event. - * - * @param UserSignedUp $event - * @return void - */ - public function handle(UserSignedUp $event) - { - if (!Utils::isNinjaProd()) { - $this->accountRepo->registerUser(Auth::user()); - } - } + /** + * Handle the event. + * + * @param UserSignedUp $event + * @return void + */ + public function handle(UserSignedUp $event) + { + $user = Auth::user(); + if (Utils::isNinjaProd()) { + $this->userMailer->sendConfirmation($user); + } elseif (Utils::isNinjaDev()) { + // do nothing + } else { + $this->accountRepo->registerNinjaUser($user); + } + + $activities = Activity::scope()->get(); + foreach ($activities as $activity) { + $activity->message = str_replace('Guest', $user->getFullName(), $activity->message); + $activity->save(); + } + + session([SESSION_COUNTER => -1]); + } } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index b910cac6a350..edb20a7c96e0 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -149,8 +149,13 @@ class Activity extends Eloquent public static function emailInvoice($invitation) { - $adjustment = 0; - $client = $invitation->invoice->client; + $invoice = $invitation->invoice; + $client = $invoice->client; + + if (!$invoice->isSent()) { + $invoice->invoice_status_id = INVOICE_STATUS_SENT; + $invoice->save(); + } $activity = Activity::getBlank($invitation); $activity->client_id = $invitation->invoice->client_id; diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 8e6f58c94c15..b42bcfbaa80b 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -39,15 +39,35 @@ class Invitation extends EntityModel if ($iframe_url) { return "{$iframe_url}/?{$this->invitation_key}"; - } else if ($this->account->subdomain) { + } elseif ($this->account->subdomain) { $url = Utils::replaceSubdomain($url, $this->account->subdomain); } return "{$url}/view/{$this->invitation_key}"; } + public function getStatus() + { + $hasValue = false; + $parts = []; + $statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed']; + + foreach ($statuses as $status) { + $field = "{$status}_date"; + $date = ''; + if ($this->$field) { + $date = Utils::dateToString($this->$field); + $hasValue = true; + } + $parts[] = trans('texts.invitation_status.' . $status) . ': ' . $date; + } + + return $hasValue ? implode($parts, '
') : false; + } + public function getName() { return $this->invitation_key; } + } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 417a227db84f..9233d3da2188 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -140,6 +140,8 @@ class Invoice extends EntityModel 'custom_taxes2', 'partial', 'has_tasks', + 'custom_text_value1', + 'custom_text_value2', ]); $this->client->setVisible([ @@ -187,6 +189,8 @@ class Invoice extends EntityModel 'custom_invoice_label2', 'pdf_email_attachment', 'show_item_taxes', + 'custom_invoice_text_label1', + 'custom_invoice_text_label2', ]); foreach ($this->invoice_items as $invoiceItem) { @@ -283,7 +287,7 @@ class Invoice extends EntityModel public function updateCachedPDF($encodedString = false) { - if (!$encodedString) { + if (!$encodedString && env('PHANTOMJS_CLOUD_KEY')) { $invitation = $this->invitations[0]; $link = $invitation->getLink(); diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index e269f91fedea..e7200609c529 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -31,70 +31,71 @@ class ContactMailer extends Mailer $invoice->updateCachedPDF(); } - $view = 'invoice'; - $accountName = $invoice->account->getDisplayName(); - $emailTemplate = $invoice->account->getEmailTemplate($reminder ?: $entityType); - $emailSubject = $invoice->account->getEmailSubject($reminder ?: $entityType); + $emailTemplate = $account->getEmailTemplate($reminder ?: $entityType); + $emailSubject = $account->getEmailSubject($reminder ?: $entityType); - $this->initClosure($invoice); - $response = false; $sent = false; foreach ($invoice->invitations as $invitation) { - if (Auth::check()) { - $user = Auth::user(); - } else { - $user = $invitation->user; - if ($invitation->user->trashed()) { - $user = $account->users()->orderBy('id')->first(); - } - } - - if (!$user->email || !$user->confirmed) { - continue; - } - - if (!$invitation->contact->email - || $invitation->contact->trashed()) { - continue; - } - - $invitation->sent_date = \Carbon::now()->toDateTimeString(); - $invitation->save(); - - $variables = [ - 'account' => $account, - 'client' => $client, - 'invitation' => $invitation, - 'amount' => $invoice->getRequestedAmount() - ]; - - $data['body'] = $this->processVariables($emailTemplate, $variables); - $data['link'] = $invitation->getLink(); - $data['entityType'] = $entityType; - $data['invoice_id'] = $invoice->id; - - $subject = $this->processVariables($emailSubject, $variables); - $fromEmail = $user->email; - $response = $this->sendTo($invitation->contact->email, $fromEmail, $accountName, $subject, $view, $data); - - if ($response === true) { + if ($this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject)) { $sent = true; - Activity::emailInvoice($invitation); } } + $account->loadLocalizationSettings(); + if ($sent === true) { - if (!$invoice->isSent()) { - $invoice->invoice_status_id = INVOICE_STATUS_SENT; - $invoice->save(); - } - - $account->loadLocalizationSettings(); Event::fire(new InvoiceSent($invoice)); } - return $response ?: trans('texts.email_error'); + return $sent ?: trans('texts.email_error'); + } + + private function sendInvitation($invitation, $invoice, $body, $subject) + { + $client = $invoice->client; + $account = $invoice->account; + + if (Auth::check()) { + $user = Auth::user(); + } else { + $user = $invitation->user; + if ($invitation->user->trashed()) { + $user = $account->users()->orderBy('id')->first(); + } + } + + if (!$user->email || !$user->confirmed) { + return false; + } + + if (!$invitation->contact->email || $invitation->contact->trashed()) { + return false; + } + + $variables = [ + 'account' => $account, + 'client' => $client, + 'invitation' => $invitation, + 'amount' => $invoice->getRequestedAmount() + ]; + + $data['body'] = $this->processVariables($body, $variables); + $data['link'] = $invitation->getLink(); + $data['entityType'] = $invoice->getEntityType(); + $data['invoiceId'] = $invoice->id; + $data['invitation'] = $invitation; + + $subject = $this->processVariables($subject, $variables); + $fromEmail = $user->email; + $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, ENTITY_INVOICE, $data); + + if ($response === true) { + Activity::emailInvoice($invitation); + return true; + } else { + return false; + } } public function sendPaymentConfirmation(Payment $payment) @@ -110,8 +111,6 @@ class ContactMailer extends Mailer $emailTemplate = $account->getEmailTemplate(ENTITY_PAYMENT); $emailSubject = $invoice->account->getEmailSubject(ENTITY_PAYMENT); - $this->initClosure($invoice); - if ($payment->invitation) { $user = $payment->invitation->user; $contact = $payment->contact; @@ -190,26 +189,7 @@ class ContactMailer extends Mailer } $str = str_replace(array_keys($variables), array_values($variables), $template); - $str = preg_replace_callback('/\{\{\$?(.*)\}\}/', $this->advancedTemplateHandler, $str); return $str; } - - private function initClosure($object) - { - $this->advancedTemplateHandler = function($match) use ($object) { - for ($i = 1; $i < count($match); $i++) { - $blobConversion = $match[$i]; - - if (isset($$blobConversion)) { - return $$blobConversion; - } else if (preg_match('/trans\(([\w\.]+)\)/', $blobConversion, $regexTranslation)) { - return trans($regexTranslation[1]); - } else if (strpos($blobConversion, '->') !== false) { - return Utils::stringToObjectResolution($object, $blobConversion); - } - - } - }; - } } diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 6162f3bd4323..dfe44015d24a 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -9,8 +9,7 @@ class Mailer { public function sendTo($toEmail, $fromEmail, $fromName, $subject, $view, $data = []) { - // https://github.com/wildbit/laravel-postmark-provider/issues/2 - if (isset($data['invoice_id']) && isset($_ENV['POSTMARK_API_TOKEN'])) { + if (isset($_ENV['POSTMARK_API_TOKEN'])) { $views = 'emails.'.$view.'_html'; } else { $views = [ @@ -20,7 +19,7 @@ class Mailer } try { - Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) { + $response = Mail::send($views, $data, function ($message) use ($toEmail, $fromEmail, $fromName, $subject, $data) { $toEmail = strtolower($toEmail); $replyEmail = $fromEmail; @@ -31,8 +30,9 @@ class Mailer ->replyTo($replyEmail, $fromName) ->subject($subject); - if (isset($data['invoice_id'])) { - $invoice = Invoice::with('account')->where('id', '=', $data['invoice_id'])->first(); + // Attach the PDF to the email + if (isset($data['invoiceId'])) { + $invoice = Invoice::with('account')->where('id', '=', $data['invoiceId'])->first(); if ($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) { $message->attach( $invoice->getPDFPath(), @@ -41,17 +41,50 @@ class Mailer } } }); - - return true; + + return $this->handleSuccess($response, $data); } catch (Exception $exception) { - Utils::logError('Email Error: ' . $exception->getMessage()); - if (isset($_ENV['POSTMARK_API_TOKEN'])) { - $response = $exception->getResponse()->getBody()->getContents(); - $response = json_decode($response); - return nl2br($response->Message); - } else { - return $exception->getMessage(); - } + return $this->handleFailure($exception); } } + + private function handleSuccess($response, $data) + { + if (isset($data['invitation'])) { + $invitation = $data['invitation']; + + // Track the Postmark message id + if (isset($_ENV['POSTMARK_API_TOKEN'])) { + $json = $response->json(); + $invitation->message_id = $json['MessageID']; + } + + $invitation->email_error = null; + $invitation->sent_date = \Carbon::now()->toDateTimeString(); + $invitation->save(); + } + + return true; + } + + private function handleFailure($exception) + { + if (isset($_ENV['POSTMARK_API_TOKEN'])) { + $response = $exception->getResponse()->getBody()->getContents(); + $response = json_decode($response); + $emailError = nl2br($response->Message); + } else { + $emailError = $exception->getMessage(); + } + + Utils::logError("Email Error: $emailError"); + + if (isset($data['invitation'])) { + $invitation = $data['invitation']; + $invitation->email_error = $emailError; + $invitation->save(); + } + + return $emailError; + } } diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php index 3f2b4290a7eb..6ca98609b639 100644 --- a/app/Ninja/Mailers/UserMailer.php +++ b/app/Ninja/Mailers/UserMailer.php @@ -2,6 +2,7 @@ use Utils; +use App\Models\Invitation; use App\Models\Invoice; use App\Models\Payment; use App\Models\User; @@ -60,4 +61,27 @@ class UserMailer extends Mailer $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } + + public function sendEmailBounced(Invitation $invitation) + { + $user = $invitation->user; + $invoice = $invitation->invoice; + $entityType = $invoice->getEntityType(); + + if (!$user->email) { + return; + } + + $subject = trans("texts.notification_{$entityType}_bounced_subject", ['invoice' => $invoice->invoice_number]); + $view = 'email_bounced'; + $data = [ + 'userName' => $user->getDisplayName(), + 'emailError' => $invitation->email_error, + 'entityType' => $entityType, + 'contactName' => $invitation->contact->getDisplayName(), + 'invoiceNumber' => $invoice->invoice_number, + ]; + + $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); + } } diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index d2e4af8b8702..d1bf2cadbced 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -6,6 +6,7 @@ use Session; use Utils; use DB; use stdClass; +use Validator; use Schema; use App\Models\AccountGateway; use App\Models\Invitation; @@ -49,6 +50,9 @@ class AccountRepository $user->first_name = $firstName; $user->last_name = $lastName; $user->email = $user->username = $email; + if (!$password) { + $password = str_random(RANDOM_KEY_LENGTH); + } $user->password = bcrypt($password); } @@ -231,7 +235,37 @@ class AccountRepository return $client; } - public function registerUser($user) + public function unlinkUserFromOauth($user) + { + $user->oauth_provider_id = null; + $user->oauth_user_id = null; + $user->save(); + } + + public function updateUserFromOauth($user, $firstName, $lastName, $email, $providerId, $oauthUserId) + { + if (!$user->registered) { + $rules = ['email' => 'email|required|unique:users,email,'.$user->id.',id']; + $validator = Validator::make(['email' => $email], $rules); + if ($validator->fails()) { + $messages = $validator->messages(); + return $messages->first('email'); + } + + $user->email = $email; + $user->first_name = $firstName; + $user->last_name = $lastName; + $user->registered = true; + } + + $user->oauth_provider_id = $providerId; + $user->oauth_user_id = $oauthUserId; + $user->save(); + + return true; + } + + public function registerNinjaUser($user) { if ($user->email == TEST_USERNAME) { return false; @@ -259,6 +293,13 @@ class AccountRepository curl_close($ch); } + public function findUserByOauth($providerId, $oauthUserId) + { + return User::where('oauth_user_id', $oauthUserId) + ->where('oauth_provider_id', $providerId) + ->first(); + } + public function findUserAccounts($userId1, $userId2 = false) { if (!Schema::hasTable('user_accounts')) { diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 054b8d19739a..ea5d2251240e 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -374,6 +374,13 @@ class InvoiceRepository $invoice->custom_taxes1 = $data['custom_taxes1'] ? true : false; $invoice->custom_taxes2 = $data['custom_taxes2'] ? true : false; + if (isset($data['custom_text_value1'])) { + $invoice->custom_text_value1 = trim($data['custom_text_value1']); + } + if (isset($data['custom_text_value2'])) { + $invoice->custom_text_value2 = trim($data['custom_text_value2']); + } + // custom fields charged taxes if ($invoice->custom_value1 && $invoice->custom_taxes1) { $total += $invoice->custom_value1; @@ -497,7 +504,9 @@ class InvoiceRepository 'custom_value2', 'custom_taxes1', 'custom_taxes2', - 'partial'] as $field) { + 'partial', + 'custom_text_value1', + 'custom_text_value2'] as $field) { $clone->$field = $invoice->$field; } @@ -615,6 +624,8 @@ class InvoiceRepository $invoice->custom_value2 = $recurInvoice->custom_value2; $invoice->custom_taxes1 = $recurInvoice->custom_taxes1; $invoice->custom_taxes2 = $recurInvoice->custom_taxes2; + $invoice->custom_text_value1 = $recurInvoice->custom_text_value1; + $invoice->custom_text_value2 = $recurInvoice->custom_text_value2; $invoice->is_amount_discount = $recurInvoice->is_amount_discount; if ($invoice->client->payment_terms != 0) { diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php new file mode 100644 index 000000000000..9244dcdb6133 --- /dev/null +++ b/app/Services/AuthService.php @@ -0,0 +1,85 @@ + SOCIAL_GOOGLE, + 2 => SOCIAL_FACEBOOK, + 3 => SOCIAL_GITHUB, + 4 => SOCIAL_LINKEDIN + ]; + + public function __construct(AccountRepository $repo) + { + $this->accountRepo = $repo; + } + + public function execute($provider, $hasCode) + { + if (!$hasCode) { + return $this->getAuthorization($provider); + } + + $socialiteUser = Socialite::driver($provider)->user(); + $providerId = AuthService::getProviderId($provider); + + if (Auth::check()) { + $user = Auth::user(); + $isRegistered = $user->registered; + + $email = $socialiteUser->email; + $oauthUserId = $socialiteUser->id; + $name = Utils::splitName($socialiteUser->name); + $result = $this->accountRepo->updateUserFromOauth($user, $name[0], $name[1], $email, $providerId, $oauthUserId); + + if ($result === true) { + if (!$isRegistered) { + event(new UserSignedUp()); + Session::flash('warning', trans('texts.success_message')); + } else { + Session::flash('message', trans('texts.updated_settings')); + return redirect()->to('/company/details'); + } + } else { + Session::flash('error', $result); + } + } else { + if ($user = $this->accountRepo->findUserByOauth($providerId, $socialiteUser->id)) { + Auth::login($user, true); + event(new UserLoggedIn()); + } else { + Session::flash('error', trans('texts.invalid_credentials')); + return redirect()->to('login'); + } + } + + $redirectTo = Input::get('redirect_to') ?: 'dashboard'; + return redirect()->to($redirectTo); + } + + private function getAuthorization($provider) + { + return Socialite::driver($provider)->redirect(); + } + + public static function getProviderId($provider) + { + return array_search(strtolower($provider), array_map('strtolower', AuthService::$providers)); + } + + public static function getProviderName($providerId) + { + return $providerId ? AuthService::$providers[$providerId] : ''; + } +} diff --git a/app/Services/EmailService.php b/app/Services/EmailService.php new file mode 100644 index 000000000000..47e130810496 --- /dev/null +++ b/app/Services/EmailService.php @@ -0,0 +1,48 @@ +userMailer = $userMailer; + } + + public function markOpened($messageId) + { + $invitation = Invitation::whereMessageId($messageId) + ->first(); + + if (!$invitation) { + return false; + } + + $invitation->opened_date = Carbon::now()->toDateTimeString(); + $invitation->save(); + + return true; + } + + public function markBounced($messageId, $error) + { + $invitation = Invitation::with('user', 'invoice', 'contact') + ->whereMessageId($messageId) + ->first(); + + if (!$invitation) { + return false; + } + + $invitation->email_error = $error; + $invitation->save(); + + $this->userMailer->sendEmailBounced($invitation); + + return true; + } +} \ No newline at end of file diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 9fa0d58e50e0..3d82cd4d39a8 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -214,15 +214,16 @@ class PaymentService { $account = $invoice->account; $invitation = $invoice->invitations->first(); $accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE); + $token = $client->getGatewayToken(); - if (!$invitation || !$accountGateway) { + if (!$invitation || !$accountGateway || !$token) { return false; } // setup the gateway/payment info $gateway = $this->createGateway($accountGateway); $details = $this->getPaymentDetails($invitation); - $details['cardReference'] = $client->getGatewayToken(); + $details['cardReference'] = $token; // submit purchase/get response $response = $gateway->purchase($details)->send(); diff --git a/composer.json b/composer.json index 4fa692e16924..4427ad4a1d00 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "guzzlehttp/guzzle": "~5.0", "laravelcollective/html": "~5.0", "wildbit/laravel-postmark-provider": "dev-master", - "Dwolla/omnipay-dwolla": "dev-master" + "Dwolla/omnipay-dwolla": "dev-master", + "laravel/socialite": "~2.0" }, "require-dev": { "phpunit/phpunit": "~4.0", diff --git a/composer.lock b/composer.lock index d7295a127c92..fa625eb6b2cd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "064b3e3608e030ecf2bfbf705b02ca5a", + "hash": "48ed13e4113a177339d3a20f31dbc6bb", "packages": [ { "name": "alfaproject/omnipay-neteller", @@ -120,12 +120,12 @@ "source": { "type": "git", "url": "https://github.com/formers/former.git", - "reference": "a2fbec9d29cf820d54dfa6f0ddb875339d77e930" + "reference": "9e75217536bf6c377318e5a6f68d4c1f96a8ae3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/formers/former/zipball/a2fbec9d29cf820d54dfa6f0ddb875339d77e930", - "reference": "a2fbec9d29cf820d54dfa6f0ddb875339d77e930", + "url": "https://api.github.com/repos/formers/former/zipball/9e75217536bf6c377318e5a6f68d4c1f96a8ae3b", + "reference": "9e75217536bf6c377318e5a6f68d4c1f96a8ae3b", "shasum": "" }, "require": { @@ -171,7 +171,7 @@ "foundation", "laravel" ], - "time": "2015-06-01 18:46:46" + "time": "2015-09-24 15:56:30" }, { "name": "anahkiasen/html-object", @@ -214,16 +214,16 @@ }, { "name": "barryvdh/laravel-debugbar", - "version": "v2.0.5", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "753106cd1f9c724d05bb48728bc652231174e279" + "reference": "23672cbbe78278ff1fdb258caa0b1de7b5d0bc0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/753106cd1f9c724d05bb48728bc652231174e279", - "reference": "753106cd1f9c724d05bb48728bc652231174e279", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/23672cbbe78278ff1fdb258caa0b1de7b5d0bc0c", + "reference": "23672cbbe78278ff1fdb258caa0b1de7b5d0bc0c", "shasum": "" }, "require": { @@ -235,7 +235,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -264,7 +264,7 @@ "profiler", "webprofiler" ], - "time": "2015-07-09 14:51:59" + "time": "2015-09-09 11:39:27" }, { "name": "barryvdh/laravel-ide-helper", @@ -702,16 +702,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.2.6", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4" + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f4a91702ca3cd2e568c3736aa031ed00c3752af4", - "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", "shasum": "" }, "require": { @@ -766,20 +766,20 @@ "docblock", "parser" ], - "time": "2015-06-17 12:21:22" + "time": "2015-08-31 12:32:49" }, { "name": "doctrine/cache", - "version": "v1.4.1", + "version": "v1.4.2", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "c9eadeb743ac6199f7eec423cb9426bc518b7b03" + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/c9eadeb743ac6199f7eec423cb9426bc518b7b03", - "reference": "c9eadeb743ac6199f7eec423cb9426bc518b7b03", + "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca", + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca", "shasum": "" }, "require": { @@ -836,7 +836,7 @@ "cache", "caching" ], - "time": "2015-04-15 00:11:59" + "time": "2015-08-31 12:36:41" }, { "name": "doctrine/collections", @@ -906,16 +906,16 @@ }, { "name": "doctrine/common", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "cd8daf2501e10c63dced7b8b9b905844316ae9d3" + "reference": "0009b8f0d4a917aabc971fb089eba80e872f83f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/cd8daf2501e10c63dced7b8b9b905844316ae9d3", - "reference": "cd8daf2501e10c63dced7b8b9b905844316ae9d3", + "url": "https://api.github.com/repos/doctrine/common/zipball/0009b8f0d4a917aabc971fb089eba80e872f83f9", + "reference": "0009b8f0d4a917aabc971fb089eba80e872f83f9", "shasum": "" }, "require": { @@ -975,20 +975,20 @@ "persistence", "spl" ], - "time": "2015-04-02 19:55:44" + "time": "2015-08-31 13:00:22" }, { "name": "doctrine/dbal", - "version": "v2.5.1", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "628c2256b646ae2417d44e063bce8aec5199d48d" + "reference": "01dbcbc5cd0a913d751418e635434a18a2f2a75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/628c2256b646ae2417d44e063bce8aec5199d48d", - "reference": "628c2256b646ae2417d44e063bce8aec5199d48d", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/01dbcbc5cd0a913d751418e635434a18a2f2a75c", + "reference": "01dbcbc5cd0a913d751418e635434a18a2f2a75c", "shasum": "" }, "require": { @@ -1046,7 +1046,7 @@ "persistence", "queryobject" ], - "time": "2015-01-12 21:52:47" + "time": "2015-09-16 16:29:33" }, { "name": "doctrine/inflector", @@ -1646,12 +1646,12 @@ "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "1124ff3c6298f0dcf9edf9156623904d7a5c3428" + "reference": "44c9a6bb292e50cf8a1e4b5030c7954c2709c089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/1124ff3c6298f0dcf9edf9156623904d7a5c3428", - "reference": "1124ff3c6298f0dcf9edf9156623904d7a5c3428", + "url": "https://api.github.com/repos/Intervention/image/zipball/44c9a6bb292e50cf8a1e4b5030c7954c2709c089", + "reference": "44c9a6bb292e50cf8a1e4b5030c7954c2709c089", "shasum": "" }, "require": { @@ -1700,7 +1700,7 @@ "thumbnail", "watermark" ], - "time": "2015-08-16 15:31:59" + "time": "2015-08-30 15:37:50" }, { "name": "ircmaxell/password-compat", @@ -2055,6 +2055,60 @@ ], "time": "2015-06-09 13:12:19" }, + { + "name": "laravel/socialite", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "5995d2c9c60b47362412a84286e2a0707e0db386" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/5995d2c9c60b47362412a84286e2a0707e0db386", + "reference": "5995d2c9c60b47362412a84286e2a0707e0db386", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~5.0|~6.0", + "illuminate/contracts": "~5.0", + "illuminate/http": "~5.0", + "illuminate/support": "~5.0", + "league/oauth1-client": "~1.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylorotwell@gmail.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "keywords": [ + "laravel", + "oauth" + ], + "time": "2015-09-24 20:59:56" + }, { "name": "laravelcollective/html", "version": "v5.0.4", @@ -2107,21 +2161,24 @@ }, { "name": "league/flysystem", - "version": "1.0.11", + "version": "1.0.15", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "c16222fdc02467eaa12cb6d6d0e65527741f6040" + "reference": "31525caf9e8772683672fefd8a1ca0c0736020f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c16222fdc02467eaa12cb6d6d0e65527741f6040", - "reference": "c16222fdc02467eaa12cb6d6d0e65527741f6040", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/31525caf9e8772683672fefd8a1ca0c0736020f4", + "reference": "31525caf9e8772683672fefd8a1ca0c0736020f4", "shasum": "" }, "require": { "php": ">=5.4.0" }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, "require-dev": { "ext-fileinfo": "*", "mockery/mockery": "~0.9", @@ -2184,7 +2241,70 @@ "sftp", "storage" ], - "time": "2015-07-28 20:41:58" + "time": "2015-09-30 22:26:59" + }, + { + "name": "league/oauth1-client", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "4d4edd9b6014f882e319231a9b3351e3a1dfdc81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/4d4edd9b6014f882e319231a9b3351e3a1dfdc81", + "reference": "4d4edd9b6014f882e319231a9b3351e3a1dfdc81", + "shasum": "" + }, + "require": { + "guzzle/guzzle": "3.*", + "php": ">=5.3.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "time": "2015-08-22 09:49:14" }, { "name": "lokielse/omnipay-alipay", @@ -2192,12 +2312,12 @@ "source": { "type": "git", "url": "https://github.com/lokielse/omnipay-alipay.git", - "reference": "f34ed8783943e01e8ed78abdbbaae487181ce3ba" + "reference": "87622e8549b50773a8db83c93c3ad9a22e618991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/f34ed8783943e01e8ed78abdbbaae487181ce3ba", - "reference": "f34ed8783943e01e8ed78abdbbaae487181ce3ba", + "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/87622e8549b50773a8db83c93c3ad9a22e618991", + "reference": "87622e8549b50773a8db83c93c3ad9a22e618991", "shasum": "" }, "require": { @@ -2233,7 +2353,7 @@ "payment", "purchase" ], - "time": "2015-08-18 17:43:49" + "time": "2015-09-15 16:43:43" }, { "name": "maximebf/debugbar", @@ -2348,16 +2468,16 @@ }, { "name": "monolog/monolog", - "version": "1.16.0", + "version": "1.17.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c0c0b4bee3aabce7182876b0d912ef2595563db7" + "reference": "0524c87587ab85bc4c2d6f5b41253ccb930a5422" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c0c0b4bee3aabce7182876b0d912ef2595563db7", - "reference": "c0c0b4bee3aabce7182876b0d912ef2595563db7", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/0524c87587ab85bc4c2d6f5b41253ccb930a5422", + "reference": "0524c87587ab85bc4c2d6f5b41253ccb930a5422", "shasum": "" }, "require": { @@ -2374,7 +2494,7 @@ "php-console/php-console": "^3.1.3", "phpunit/phpunit": "~4.5", "phpunit/phpunit-mock-objects": "2.3.0", - "raven/raven": "~0.8", + "raven/raven": "~0.11", "ruflin/elastica": ">=0.90 <3.0", "swiftmailer/swiftmailer": "~5.3", "videlalvaro/php-amqplib": "~2.4" @@ -2420,7 +2540,7 @@ "logging", "psr-3" ], - "time": "2015-08-09 17:44:44" + "time": "2015-08-31 09:17:37" }, { "name": "mtdowling/cron-expression", @@ -2515,16 +2635,16 @@ }, { "name": "nikic/php-parser", - "version": "v1.4.0", + "version": "v1.4.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "196f177cfefa0f1f7166c0a05d8255889be12418" + "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/196f177cfefa0f1f7166c0a05d8255889be12418", - "reference": "196f177cfefa0f1f7166c0a05d8255889be12418", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", + "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", "shasum": "" }, "require": { @@ -2556,7 +2676,7 @@ "parser", "php" ], - "time": "2015-07-14 17:31:05" + "time": "2015-09-19 14:15:08" }, { "name": "omnipay/2checkout", @@ -3196,16 +3316,16 @@ }, { "name": "omnipay/gocardless", - "version": "v2.1.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-gocardless.git", - "reference": "106c2cc2f992813319c86f45863347c238771548" + "reference": "1c0bebdcc32d89fd243e1183028d2d50316e8bb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-gocardless/zipball/106c2cc2f992813319c86f45863347c238771548", - "reference": "106c2cc2f992813319c86f45863347c238771548", + "url": "https://api.github.com/repos/thephpleague/omnipay-gocardless/zipball/1c0bebdcc32d89fd243e1183028d2d50316e8bb1", + "reference": "1c0bebdcc32d89fd243e1183028d2d50316e8bb1", "shasum": "" }, "require": { @@ -3250,7 +3370,7 @@ "pay", "payment" ], - "time": "2014-09-17 00:39:16" + "time": "2015-09-24 14:44:29" }, { "name": "omnipay/manual", @@ -4652,16 +4772,16 @@ }, { "name": "symfony/class-loader", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/ClassLoader.git", - "reference": "2fccbc544997340808801a7410cdcb96dd12edc4" + "url": "https://github.com/symfony/class-loader.git", + "reference": "d957ea6295d7016e20d7eff33a6c1deef819c0d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/2fccbc544997340808801a7410cdcb96dd12edc4", - "reference": "2fccbc544997340808801a7410cdcb96dd12edc4", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/d957ea6295d7016e20d7eff33a6c1deef819c0d4", + "reference": "d957ea6295d7016e20d7eff33a6c1deef819c0d4", "shasum": "" }, "require": { @@ -4698,7 +4818,7 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2015-06-25 12:52:11" + "time": "2015-08-26 17:56:37" }, { "name": "symfony/console", @@ -4821,16 +4941,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", + "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", "shasum": "" }, "require": { @@ -4875,20 +4995,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-06-18 19:21:56" + "time": "2015-09-22 13:49:29" }, { "name": "symfony/filesystem", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/Filesystem.git", - "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8" + "url": "https://github.com/symfony/filesystem.git", + "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Filesystem/zipball/2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", - "reference": "2d7b2ddaf3f548f4292df49a99d19c853d43f0b8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/a17f8a17c20e8614c15b8e116e2f4bcde102cfab", + "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab", "shasum": "" }, "require": { @@ -4924,7 +5044,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-09-09 17:42:36" }, { "name": "symfony/finder", @@ -5513,12 +5633,12 @@ "source": { "type": "git", "url": "https://github.com/webpatser/laravel-countries.git", - "reference": "3142b86c71908a8e63dec07bc8fe48abff9902dd" + "reference": "e29dcce821f2c4a522e35483c38632ca534db4ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webpatser/laravel-countries/zipball/3142b86c71908a8e63dec07bc8fe48abff9902dd", - "reference": "3142b86c71908a8e63dec07bc8fe48abff9902dd", + "url": "https://api.github.com/repos/webpatser/laravel-countries/zipball/e29dcce821f2c4a522e35483c38632ca534db4ee", + "reference": "e29dcce821f2c4a522e35483c38632ca534db4ee", "shasum": "" }, "require": { @@ -5557,7 +5677,7 @@ "iso_3166_3", "laravel" ], - "time": "2015-06-10 07:52:21" + "time": "2015-08-21 12:12:14" }, { "name": "wildbit/laravel-postmark-provider", @@ -5679,23 +5799,23 @@ }, { "name": "codeception/codeception", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "521adbb2ee34e9debdd8508a2c41ab2b5c2f042b" + "reference": "cd810cb78a869408602e17271f9b7368b09a7ca8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/521adbb2ee34e9debdd8508a2c41ab2b5c2f042b", - "reference": "521adbb2ee34e9debdd8508a2c41ab2b5c2f042b", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/cd810cb78a869408602e17271f9b7368b09a7ca8", + "reference": "cd810cb78a869408602e17271f9b7368b09a7ca8", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "facebook/webdriver": ">=1.0.1", - "guzzlehttp/guzzle": ">=4.0|<7.0", + "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", "php": ">=5.4.0", "phpunit/phpunit": "~4.8.0", @@ -5755,7 +5875,7 @@ "functional testing", "unit testing" ], - "time": "2015-08-09 13:48:55" + "time": "2015-10-02 09:38:59" }, { "name": "doctrine/instantiator", @@ -5942,16 +6062,16 @@ }, { "name": "phpspec/phpspec", - "version": "2.2.1", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8" + "reference": "36635a903bdeb54899d7407bc95610501fd98559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/e9a40577323e67f1de2e214abf32976a0352d8f8", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/36635a903bdeb54899d7407bc95610501fd98559", + "reference": "36635a903bdeb54899d7407bc95610501fd98559", "shasum": "" }, "require": { @@ -5963,15 +6083,14 @@ "symfony/console": "~2.3", "symfony/event-dispatcher": "~2.1", "symfony/finder": "~2.1", - "symfony/process": "~2.1", + "symfony/process": "^2.6", "symfony/yaml": "~2.1" }, "require-dev": { "behat/behat": "^3.0.11", "bossa/phpspec2-expect": "~1.0", "phpunit/phpunit": "~4.4", - "symfony/filesystem": "~2.1", - "symfony/process": "~2.1" + "symfony/filesystem": "~2.1" }, "suggest": { "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" @@ -6016,7 +6135,7 @@ "testing", "tests" ], - "time": "2015-05-30 15:21:40" + "time": "2015-09-07 07:07:37" }, { "name": "phpspec/prophecy", @@ -6080,16 +6199,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "2.2.2", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c" + "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c", - "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef1ca6835468857944d5c3b48fa503d5554cff2f", + "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f", "shasum": "" }, "require": { @@ -6138,7 +6257,7 @@ "testing", "xunit" ], - "time": "2015-08-04 03:42:39" + "time": "2015-09-14 06:51:16" }, { "name": "phpunit/php-file-iterator", @@ -6271,16 +6390,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.4.6", + "version": "1.4.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b" + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3ab72c62e550370a6cd5dc873e1a04ab57562f5b", - "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", "shasum": "" }, "require": { @@ -6316,20 +6435,20 @@ "keywords": [ "tokenizer" ], - "time": "2015-08-16 08:51:00" + "time": "2015-09-15 10:49:45" }, { "name": "phpunit/phpunit", - "version": "4.8.4", + "version": "4.8.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "55bf1d6092b0e13a1f26bd5eaffeef3d8ad85ea7" + "reference": "463163747474815c5ccd4ae12b5b355ec12158e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/55bf1d6092b0e13a1f26bd5eaffeef3d8ad85ea7", - "reference": "55bf1d6092b0e13a1f26bd5eaffeef3d8ad85ea7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/463163747474815c5ccd4ae12b5b355ec12158e8", + "reference": "463163747474815c5ccd4ae12b5b355ec12158e8", "shasum": "" }, "require": { @@ -6388,24 +6507,24 @@ "testing", "xunit" ], - "time": "2015-08-15 04:21:23" + "time": "2015-10-01 09:14:30" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.6", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/18dfbcb81d05e2296c0bcddd4db96cade75e6f42", - "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { - "doctrine/instantiator": "~1.0,>=1.0.2", + "doctrine/instantiator": "^1.0.2", "php": ">=5.3.3", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" @@ -6444,7 +6563,7 @@ "mock", "xunit" ], - "time": "2015-07-10 06:54:24" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", @@ -6819,16 +6938,16 @@ }, { "name": "symfony/browser-kit", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/BrowserKit.git", - "reference": "176905d3d74c2f99e6ab70f4f5a89460532495ae" + "url": "https://github.com/symfony/browser-kit.git", + "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/BrowserKit/zipball/176905d3d74c2f99e6ab70f4f5a89460532495ae", - "reference": "176905d3d74c2f99e6ab70f4f5a89460532495ae", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/277a2457776d4cc25706fbdd9d1e4ab2dac884e4", + "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4", "shasum": "" }, "require": { @@ -6870,20 +6989,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-09-06 08:36:38" }, { "name": "symfony/css-selector", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/CssSelector.git", - "reference": "0b5c07b516226b7dd32afbbc82fe547a469c5092" + "url": "https://github.com/symfony/css-selector.git", + "reference": "abe19cc0429a06be0c133056d1f9859854860970" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/CssSelector/zipball/0b5c07b516226b7dd32afbbc82fe547a469c5092", - "reference": "0b5c07b516226b7dd32afbbc82fe547a469c5092", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/abe19cc0429a06be0c133056d1f9859854860970", + "reference": "abe19cc0429a06be0c133056d1f9859854860970", "shasum": "" }, "require": { @@ -6923,20 +7042,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2015-05-15 13:33:16" + "time": "2015-09-22 13:49:29" }, { "name": "symfony/dom-crawler", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/DomCrawler.git", - "reference": "9dabece63182e95c42b06967a0d929a5df78bc35" + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "2e185ca136399f902b948694987e62c80099c052" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/DomCrawler/zipball/9dabece63182e95c42b06967a0d929a5df78bc35", - "reference": "9dabece63182e95c42b06967a0d929a5df78bc35", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2e185ca136399f902b948694987e62c80099c052", + "reference": "2e185ca136399f902b948694987e62c80099c052", "shasum": "" }, "require": { @@ -6976,20 +7095,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-09-20 21:13:58" }, { "name": "symfony/yaml", - "version": "v2.7.3", + "version": "v2.7.5", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" + "url": "https://github.com/symfony/yaml.git", + "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", + "url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770", + "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770", "shasum": "" }, "require": { @@ -7025,7 +7144,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "time": "2015-09-14 14:14:09" } ], "aliases": [], diff --git a/config/app.php b/config/app.php index 5b6fdff42998..00fd09f24e75 100644 --- a/config/app.php +++ b/config/app.php @@ -149,6 +149,7 @@ return [ 'Webpatser\Countries\CountriesServiceProvider', 'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider', 'Illuminate\Html\HtmlServiceProvider', + 'Laravel\Socialite\SocialiteServiceProvider', /* * Application Service Providers... @@ -242,6 +243,7 @@ return [ 'Countries' => 'Webpatser\Countries\CountriesFacade', 'Carbon' => 'Carbon\Carbon', 'Rocketeer' => 'Rocketeer\Facades\Rocketeer', + 'Socialite' => 'Laravel\Socialite\Facades\Socialite', ], diff --git a/config/services.php b/config/services.php index c76b0832db58..f353fddd28c7 100644 --- a/config/services.php +++ b/config/services.php @@ -36,4 +36,28 @@ return [ 'secret' => '', ], + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => 'http://ninja.dev/auth/github' + ], + + 'google' => [ + 'client_id' => '640903115046-dd09j2q24lcc3ilrrv5f2ft2i3n0sreg.apps.googleusercontent.com', + 'client_secret' => 'Vsfhldq7mRxsCXQTQI8U_4Ua', + 'redirect' => 'http://ninja.dev/auth/google', + ], + + 'facebook' => [ + 'client_id' => '635126583203143', + 'client_secret' => '7aa7c391019f2ece3c6aa90f4c9b1485', + 'redirect' => 'http://ninja.dev/auth/facebook', + ], + + 'linkedin' => [ + 'client_id' => '778is2j21w25xj', + 'client_secret' => 'DvDExxfBLXUtxc81', + 'redirect' => 'http://ninja.dev/auth/linkedin', + ], + ]; diff --git a/database/migrations/2015_10_07_135651_add_social_login.php b/database/migrations/2015_10_07_135651_add_social_login.php new file mode 100644 index 000000000000..ebe1975dc760 --- /dev/null +++ b/database/migrations/2015_10_07_135651_add_social_login.php @@ -0,0 +1,64 @@ +string('oauth_user_id')->nullable(); + $table->unsignedInteger('oauth_provider_id')->nullable(); + }); + + Schema::table('accounts', function ($table) { + $table->string('custom_invoice_text_label1')->nullable(); + $table->string('custom_invoice_text_label2')->nullable(); + }); + + Schema::table('invoices', function ($table) { + $table->string('custom_text_value1')->nullable(); + $table->string('custom_text_value2')->nullable(); + }); + + Schema::table('invitations', function ($table) { + $table->timestamp('opened_date')->nullable(); + $table->string('message_id')->nullable(); + $table->text('email_error')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function ($table) { + $table->dropColumn('oauth_user_id'); + $table->dropColumn('oauth_provider_id'); + }); + + Schema::table('accounts', function ($table) { + $table->dropColumn('custom_invoice_text_label1'); + $table->dropColumn('custom_invoice_text_label2'); + }); + + Schema::table('invoices', function ($table) { + $table->dropColumn('custom_text_value1'); + $table->dropColumn('custom_text_value2'); + }); + + Schema::table('invitations', function ($table) { + $table->dropColumn('opened_date'); + $table->dropColumn('message_id'); + $table->dropColumn('email_error'); + }); + } +} diff --git a/public/css/built.css b/public/css/built.css index d49558d9566e..22367aad6388 100644 --- a/public/css/built.css +++ b/public/css/built.css @@ -3332,3 +3332,7 @@ ul.user-accounts a:hover div.remove { visibility: visible; } +.tooltip-inner { + text-align:left; + width: 350px; +} \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index cdd02ee90740..ddabc40c4928 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -982,3 +982,7 @@ ul.user-accounts a:hover div.remove { visibility: visible; } +.tooltip-inner { + text-align:left; + width: 350px; +} \ No newline at end of file diff --git a/public/js/built.js b/public/js/built.js index 1f40652e3845..3b1be8b9d74f 100644 --- a/public/js/built.js +++ b/public/js/built.js @@ -30400,6 +30400,26 @@ if (window.ko) { ko.applyBindingsToNode(element, { attr: { placeholder: underlyingObservable } } ); } }; + + ko.bindingHandlers.tooltip = { + init: function(element, valueAccessor) { + var local = ko.utils.unwrapObservable(valueAccessor()), + options = {}; + + ko.utils.extend(options, ko.bindingHandlers.tooltip.options); + ko.utils.extend(options, local); + + $(element).tooltip(options); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $(element).tooltip("destroy"); + }); + }, + options: { + placement: "bottom", + trigger: "hover" + } + }; } function getContactDisplayName(contact) @@ -31995,6 +32015,18 @@ NINJA.invoiceDetails = function(invoice) { ] ]; + if (invoice.custom_text_value1) { + data.push([ + {text: invoice.account.custom_invoice_text_label1}, + {text: invoice.custom_text_value1} + ]) + } + if (invoice.custom_text_value2) { + data.push([ + {text: invoice.account.custom_invoice_text_label2}, + {text: invoice.custom_text_value2} + ]) + } var isPartial = NINJA.parseFloat(invoice.partial); @@ -32002,18 +32034,18 @@ NINJA.invoiceDetails = function(invoice) { data.push([ {text: invoiceLabels.total}, {text: formatMoney(invoice.amount, invoice.client.currency_id)} - ]); + ]); } else if (isPartial) { data.push([ {text: invoiceLabels.total}, {text: formatMoney(invoice.total_amount, invoice.client.currency_id)} - ]); + ]); } data.push([ {text: isPartial ? invoiceLabels.amount_due : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, {text: formatMoney(invoice.balance_amount, invoice.client.currency_id), style: ['invoiceDetailBalanceDue']} - ]) + ]) return NINJA.prepareDataPairs(data, 'invoiceDetails'); } diff --git a/public/js/pdf.pdfmake.js b/public/js/pdf.pdfmake.js index bad3cea5d5e7..fb0588113fc5 100644 --- a/public/js/pdf.pdfmake.js +++ b/public/js/pdf.pdfmake.js @@ -442,6 +442,18 @@ NINJA.invoiceDetails = function(invoice) { ] ]; + if (invoice.custom_text_value1) { + data.push([ + {text: invoice.account.custom_invoice_text_label1}, + {text: invoice.custom_text_value1} + ]) + } + if (invoice.custom_text_value2) { + data.push([ + {text: invoice.account.custom_invoice_text_label2}, + {text: invoice.custom_text_value2} + ]) + } var isPartial = NINJA.parseFloat(invoice.partial); @@ -449,18 +461,18 @@ NINJA.invoiceDetails = function(invoice) { data.push([ {text: invoiceLabels.total}, {text: formatMoney(invoice.amount, invoice.client.currency_id)} - ]); + ]); } else if (isPartial) { data.push([ {text: invoiceLabels.total}, {text: formatMoney(invoice.total_amount, invoice.client.currency_id)} - ]); + ]); } data.push([ {text: isPartial ? invoiceLabels.amount_due : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']}, {text: formatMoney(invoice.balance_amount, invoice.client.currency_id), style: ['invoiceDetailBalanceDue']} - ]) + ]) return NINJA.prepareDataPairs(data, 'invoiceDetails'); } diff --git a/public/js/script.js b/public/js/script.js index 915d9eb17424..61d58c7f8a74 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -522,6 +522,26 @@ if (window.ko) { ko.applyBindingsToNode(element, { attr: { placeholder: underlyingObservable } } ); } }; + + ko.bindingHandlers.tooltip = { + init: function(element, valueAccessor) { + var local = ko.utils.unwrapObservable(valueAccessor()), + options = {}; + + ko.utils.extend(options, ko.bindingHandlers.tooltip.options); + ko.utils.extend(options, local); + + $(element).tooltip(options); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $(element).tooltip("destroy"); + }); + }, + options: { + placement: "bottom", + trigger: "hover" + } + }; } function getContactDisplayName(contact) diff --git a/resources/lang/da/texts.php b/resources/lang/da/texts.php index 61a9405ecfcb..34fe4f51939f 100644 --- a/resources/lang/da/texts.php +++ b/resources/lang/da/texts.php @@ -796,5 +796,24 @@ 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); \ No newline at end of file diff --git a/resources/lang/de/texts.php b/resources/lang/de/texts.php index 9b8aad70c66f..0ab602f7279c 100644 --- a/resources/lang/de/texts.php +++ b/resources/lang/de/texts.php @@ -795,5 +795,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index e86e0bc7e6ad..84e9e16fc785 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -305,7 +305,7 @@ return array( 'email_taken' => 'The email address is already registered', 'working' => 'Working', 'success' => 'Success', - 'success_message' => 'You have successfully registered. Please visit the link in the account confirmation email to verify your email address.', + 'success_message' => 'You have successfully registered! Please visit the link in the account confirmation email to verify your email address.', 'erase_data' => 'This will permanently erase your data.', 'password' => 'Password', @@ -320,7 +320,7 @@ return array( -- email us at contact@invoiceninja.com', 'unsaved_changes' => 'You have unsaved changes', - 'custom_fields' => 'Custom fields', + 'custom_fields' => 'Custom Fields', 'company_fields' => 'Company Fields', 'client_fields' => 'Client Fields', 'field_label' => 'Field Label', @@ -769,7 +769,7 @@ return array( 'iframe_url' => 'Website', 'iframe_url_help1' => 'Copy the following code to a page on your site.', - 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', + 'iframe_url_help2' => 'Currently only supported with on-site gateways (ie, Stripe and Authorize.net). You can test the feature by clicking \'View as recipient\' for an invoice.', 'auto_bill' => 'Auto Bill', 'military_time' => '24 Hour Time', @@ -795,6 +795,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/lang/es/texts.php b/resources/lang/es/texts.php index c08cf53f88c2..4b8071a8fd71 100644 --- a/resources/lang/es/texts.php +++ b/resources/lang/es/texts.php @@ -773,5 +773,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); diff --git a/resources/lang/es_ES/texts.php b/resources/lang/es_ES/texts.php index 5d48d3a9a2ce..af97cee24fd6 100644 --- a/resources/lang/es_ES/texts.php +++ b/resources/lang/es_ES/texts.php @@ -795,5 +795,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); \ No newline at end of file diff --git a/resources/lang/fr/texts.php b/resources/lang/fr/texts.php index 8f0ae4e90b80..d97faea24311 100644 --- a/resources/lang/fr/texts.php +++ b/resources/lang/fr/texts.php @@ -787,5 +787,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); diff --git a/resources/lang/fr_CA/texts.php b/resources/lang/fr_CA/texts.php index 6257451a3f31..dd014c28161c 100644 --- a/resources/lang/fr_CA/texts.php +++ b/resources/lang/fr_CA/texts.php @@ -788,5 +788,24 @@ return array( 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + ); diff --git a/resources/lang/it/texts.php b/resources/lang/it/texts.php index 3730c16fa5b7..a3022fd5d9da 100644 --- a/resources/lang/it/texts.php +++ b/resources/lang/it/texts.php @@ -789,5 +789,24 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/lang/lt/texts.php b/resources/lang/lt/texts.php index a9a26e74c7a6..baa7f7c9e3dd 100644 --- a/resources/lang/lt/texts.php +++ b/resources/lang/lt/texts.php @@ -789,7 +789,6 @@ return array( 'reset' => 'Reset', 'invoice_not_found' => 'The requested invoice is not available', - 'referral_program' => 'Referral Program', 'referral_code' => 'Referral Code', 'last_sent_on' => 'Last sent on :date', @@ -797,6 +796,25 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/lang/nb_NO/texts.php b/resources/lang/nb_NO/texts.php index de8643b514dd..ae0f3691c751 100644 --- a/resources/lang/nb_NO/texts.php +++ b/resources/lang/nb_NO/texts.php @@ -794,5 +794,24 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); \ No newline at end of file diff --git a/resources/lang/nl/texts.php b/resources/lang/nl/texts.php index af7f3297ccab..e72ec1aefc20 100644 --- a/resources/lang/nl/texts.php +++ b/resources/lang/nl/texts.php @@ -789,5 +789,24 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/lang/pt_BR/texts.php b/resources/lang/pt_BR/texts.php index 2c5c00f7b414..29f7084859aa 100644 --- a/resources/lang/pt_BR/texts.php +++ b/resources/lang/pt_BR/texts.php @@ -789,5 +789,24 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/lang/sv/texts.php b/resources/lang/sv/texts.php index ca7bbdc3e2af..5754ba7647e0 100644 --- a/resources/lang/sv/texts.php +++ b/resources/lang/sv/texts.php @@ -792,5 +792,24 @@ return array( 'page_expire' => 'This page will expire soon, :click_here to keep working', 'upcoming_quotes' => 'Upcoming Quotes', 'expired_quotes' => 'Expired Quotes', + + 'sign_up_using' => 'Sign up using', + 'invalid_credentials' => 'These credentials do not match our records', + 'show_all_options' => 'Show all options', + 'user_details' => 'User Details', + 'oneclick_login' => 'One-Click Login', + 'disable' => 'Disable', + 'invoice_quote_number' => 'Invoice and Quote Numbers', + 'invoice_charges' => 'Invoice Charges', + + 'invitation_status' => [ + 'sent' => 'Email Sent', + 'opened' => 'Email Openend', + 'viewed' => 'Invoice Viewed', + ], + 'notification_invoice_bounced' => 'We were unable to deliver Invoice :invoice to :contact.', + 'notification_invoice_bounced_subject' => 'Unable to deliver Invoice :invoice', + 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', + 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', ); diff --git a/resources/views/accounts/details.blade.php b/resources/views/accounts/details.blade.php index 6b14fd06bf26..ccbda5019553 100644 --- a/resources/views/accounts/details.blade.php +++ b/resources/views/accounts/details.blade.php @@ -18,15 +18,13 @@ {{ Former::populate($account) }} {{ Former::populateField('military_time', intval($account->military_time)) }} - @if ($showUser) - {{ Former::populateField('first_name', $primaryUser->first_name) }} - {{ Former::populateField('last_name', $primaryUser->last_name) }} - {{ Former::populateField('email', $primaryUser->email) }} - {{ Former::populateField('phone', $primaryUser->phone) }} - @if (Utils::isNinjaDev()) - {{ Former::populateField('dark_mode', intval($primaryUser->dark_mode)) }} - @endif - @endif + {{ Former::populateField('first_name', $user->first_name) }} + {{ Former::populateField('last_name', $user->last_name) }} + {{ Former::populateField('email', $user->email) }} + {{ Former::populateField('phone', $user->phone) }} + @if (Utils::isNinjaDev()) + {{ Former::populateField('dark_mode', intval($user->dark_mode)) }} + @endif
@@ -76,31 +74,41 @@
- @if ($showUser)
-

{!! trans('texts.primary_user') !!}

+

{!! trans('texts.user_details') !!}

{!! Former::text('first_name') !!} {!! Former::text('last_name') !!} {!! Former::text('email') !!} {!! Former::text('phone') !!} - @if (Utils::isNinja() && $primaryUser->confirmed) - @if ($primaryUser->referral_code) + + @if (Utils::isNinja()) + {!! Former::plaintext('oneclick_login')->value( + $user->oauth_provider_id ? + $oauthProviderName . ' - ' . link_to('#', trans('texts.disable'), ['onclick' => 'disableSocialLogin()']) : + DropdownButton::primary(trans('texts.enable'))->withContents($oauthLoginUrls)->small() + ) !!} + @endif + + @if (Utils::isNinja() && $user->confirmed) + @if ($user->referral_code) {!! Former::plaintext('referral_code') - ->value($primaryUser->referral_code . ' ' . Icon::create('question-sign') . '') !!} + ->value($user->referral_code . ' ' . Icon::create('question-sign') . '') !!} @else {!! Former::checkbox('referral_code') ->text(trans('texts.enable') . ' ' . Icon::create('question-sign') . '') !!} @endif @endif + @if (false && Utils::isNinjaDev()) {!! Former::checkbox('dark_mode')->text(trans('texts.dark_mode_help')) !!} @endif @if (Utils::isNinja()) - @if (Auth::user()->confirmed) +
+ @if (Auth::user()->confirmed) {!! Former::actions( Button::primary(trans('texts.change_password'))->small()->withAttributes(['onclick'=>'showChangePassword()'])) !!} @elseif (Auth::user()->registered) {!! Former::actions( Button::primary(trans('texts.resend_confirmation'))->asLinkTo(URL::to('/resend_confirmation'))->small() ) !!} @@ -108,7 +116,6 @@ @endif
- @endif
@@ -214,8 +221,9 @@ $('#passwordModal').on('shown.bs.modal', function () { $('#current_password').focus(); - }) + }) + localStorage.setItem('auth_provider', '{{ strtolower($oauthProviderName) }}'); }); function deleteLogo() { @@ -294,6 +302,14 @@ } }); } + + function disableSocialLogin() { + if (!confirm("{!! trans('texts.are_you_sure') !!}")) { + return; + } + + window.location = '{{ URL::to('/auth_unlink') }}'; + } @stop diff --git a/resources/views/accounts/invoice_settings.blade.php b/resources/views/accounts/invoice_settings.blade.php index 8f2da4c9fe7e..ea878b361bfc 100644 --- a/resources/views/accounts/invoice_settings.blade.php +++ b/resources/views/accounts/invoice_settings.blade.php @@ -17,96 +17,115 @@ @parent @include('accounts.nav_advanced') - {!! Former::open()->addClass('warn-on-exit') !!} + {!! Former::open()->addClass('col-md-8 col-md-offset-2 warn-on-exit') !!} {{ Former::populate($account) }} {{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }} {{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }} {{ Former::populateField('share_counter', intval($account->share_counter)) }} {{ Former::populateField('pdf_email_attachment', intval($account->pdf_email_attachment)) }} -
-
-
-
-

{!! trans('texts.invoice_fields') !!}

-
-
- {!! Former::text('custom_invoice_label1')->label(trans('texts.field_label')) - ->append(Former::checkbox('custom_invoice_taxes1')->raw() . trans('texts.charge_taxes')) !!} - {!! Former::text('custom_invoice_label2')->label(trans('texts.field_label')) - ->append(Former::checkbox('custom_invoice_taxes2')->raw() . ' ' . trans('texts.charge_taxes')) !!} -
-
- -
-
-

{!! trans('texts.client_fields') !!}

-
-
- {!! Former::text('custom_client_label1')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_client_label2')->label(trans('texts.field_label')) !!} -
-
- - -
-
-

{!! trans('texts.company_fields') !!}

-
-
- {!! Former::text('custom_label1')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_value1')->label(trans('texts.field_value')) !!} -

 

- {!! Former::text('custom_label2')->label(trans('texts.field_label')) !!} - {!! Former::text('custom_value2')->label(trans('texts.field_value')) !!} +
+

{!! trans('texts.email_settings') !!}

-
- -
-
- -
-
-

{!! trans('texts.email_settings') !!}

-
@if (Utils::isNinja()) - {{ Former::setOption('capitalize_translations', false) }} - {!! Former::text('subdomain')->placeholder(trans('texts.www'))->onchange('onSubdomainChange()') !!} - {!! Former::text('iframe_url')->placeholder('http://invoices.example.com/') - ->onchange('onDomainChange()')->appendIcon('question-sign')->addGroupClass('iframe_url') !!} + {{ Former::setOption('capitalize_translations', false) }} + {!! Former::text('subdomain')->placeholder(trans('texts.www'))->onchange('onSubdomainChange()') !!} + {!! Former::text('iframe_url')->placeholder('http://invoices.example.com/') + ->onchange('onDomainChange()')->appendIcon('question-sign')->addGroupClass('iframe_url') !!} @endif {!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
-
-

{!! trans('texts.invoice_number') !!}

-
-
- {!! Former::text('invoice_number_prefix')->label(trans('texts.prefix')) !!} - {!! Former::text('invoice_number_counter')->label(trans('texts.counter')) !!} +
+

{!! trans('texts.invoice_quote_number') !!}

+
+
+ +
+
+
+ {!! Former::text('invoice_number_prefix')->label(trans('texts.prefix')) !!} + {!! Former::text('invoice_number_counter')->label(trans('texts.counter')) !!} +
+
+
+
+ {!! Former::text('quote_number_prefix')->label(trans('texts.prefix')) !!} + {!! Former::text('quote_number_counter')->label(trans('texts.counter')) + ->append(Former::checkbox('share_counter')->raw()->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) !!} +
+
+
-
-

{!! trans('texts.quote_number') !!}

-
-
- {!! Former::text('quote_number_prefix')->label(trans('texts.prefix')) !!} - {!! Former::text('quote_number_counter')->label(trans('texts.counter')) - ->append(Former::checkbox('share_counter')->raw()->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) !!} -
+
+

{!! trans('texts.custom_fields') !!}

+
+
+ + +
+
+
+ + {!! Former::text('custom_client_label1')->label(trans('texts.field_label')) !!} + {!! Former::text('custom_client_label2')->label(trans('texts.field_label')) !!} + +
+
+
+
+ + {!! Former::text('custom_label1')->label(trans('texts.field_label')) !!} + {!! Former::text('custom_value1')->label(trans('texts.field_value')) !!} +

 

+ {!! Former::text('custom_label2')->label(trans('texts.field_label')) !!} + {!! Former::text('custom_value2')->label(trans('texts.field_value')) !!} + +
+
+
+
+ + {!! Former::text('custom_invoice_text_label1')->label(trans('texts.field_label')) !!} + {!! Former::text('custom_invoice_text_label2')->label(trans('texts.field_label')) !!} + +
+
+
+
+ + {!! Former::text('custom_invoice_label1')->label(trans('texts.field_label')) + ->append(Former::checkbox('custom_invoice_taxes1')->raw() . trans('texts.charge_taxes')) !!} + {!! Former::text('custom_invoice_label2')->label(trans('texts.field_label')) + ->append(Former::checkbox('custom_invoice_taxes2')->raw() . trans('texts.charge_taxes')) !!} + +
+
+
+
- -
-
@if (Auth::user()->isPro())
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index faf732edf63a..92ae21058449 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -64,10 +64,9 @@ @include('partials.warn_session', ['redirectTo' => '/login']) - {!! Former::open('login') ->rules(['email' => 'required|email', 'password' => 'required']) - ->addClass('form-signin warn-on-exit') !!} + ->addClass('form-signin') !!} {{ Former::populateField('remember', 'true') }} - @endif + @endif @if (Session::has('warning'))
{{ Session::get('warning') }}
@@ -160,7 +163,15 @@ } else { $('#email').focus(); } + + // If they're using OAuth we'll show just their provider button + var authProvider = localStorage.getItem('auth_provider'); + if (authProvider) { + //$('#loginButton').removeClass('btn-success').addClass('btn-primary'); + $('#' + authProvider + 'LoginButton').removeClass('btn-primary').addClass('btn-success'); + } }) + @endsection \ No newline at end of file diff --git a/resources/views/emails/email_bounced_html.blade.php b/resources/views/emails/email_bounced_html.blade.php new file mode 100644 index 000000000000..b80c35dbf748 --- /dev/null +++ b/resources/views/emails/email_bounced_html.blade.php @@ -0,0 +1,20 @@ + + + + + + + @if (false) + @include('emails.view_action', ['link' => $invoiceLink, 'entityType' => $entityType]) + @endif + {{ trans('texts.email_salutation', ['name' => $userName]) }}

+ + {{ trans("texts.notification_{$entityType}_bounced", ['contact' => $contactName, 'invoice' => $invoiceNumber]) }}

+ + {{ $emailError }}

+ + {{ trans('texts.email_signature') }}
+ {{ trans('texts.email_from') }}

+ + + \ No newline at end of file diff --git a/resources/views/emails/email_bounced_text.blade.php b/resources/views/emails/email_bounced_text.blade.php new file mode 100644 index 000000000000..f5dc30869f12 --- /dev/null +++ b/resources/views/emails/email_bounced_text.blade.php @@ -0,0 +1,8 @@ +{!! trans('texts.email_salutation', ['name' => $userName]) !!} + +{!! trans("texts.notification_{$entityType}_bounced", ['contact' => $contactName, 'invoice' => $invoiceNumber]) !!} + +{!! $emailError !!} + +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!} \ No newline at end of file diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 3e1c52f34f13..04e26cc03d2d 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -159,8 +159,12 @@ } } - function showSignUp() { - $('#signUpModal').modal('show'); + function showSignUp() { + $('#signUpModal').modal('show'); + } + + function hideSignUp() { + $('#signUpModal').modal('hide'); } NINJA.proPlanFeature = ''; @@ -239,6 +243,10 @@ @endif } + function setSignupEnabled(enabled) { + $('.signup-form input[type=text], .signup-form button').prop('disabled', !enabled); + } + $(function() { window.setTimeout(function() { $(".alert-hide").fadeOut(); @@ -253,17 +261,18 @@ $('#search').css('width', '{{ Utils::isEnglish() ? 256 : 216 }}px'); $('ul.navbar-right').hide(); if (!window.hasOwnProperty('searchData')) { - $.get('{{ URL::route('getSearchData') }}', function(data) { - window.searchData = true; + trackEvent('/activity', '/search'); + $.get('{{ URL::route('getSearchData') }}', function(data) { + window.searchData = true; var datasets = []; for (var type in data) - { - if (!data.hasOwnProperty(type)) continue; + { + if (!data.hasOwnProperty(type)) continue; datasets.push({ name: type, - header: ' ' + type + '', + header: ' ' + type + '', local: data[type] - }); + }); } if (datasets.length == 0) { return; @@ -306,9 +315,6 @@ @endif $('ul.navbar-settings, ul.navbar-history').hover(function () { - //$('.user-accounts').find('li').hide(); - //$('.user-accounts').css({display: 'none'}); - //console.log($('.user-accounts').dropdown('')) if ($('.user-accounts').css('display') == 'block') { $('.user-accounts').dropdown('toggle'); } @@ -320,6 +326,14 @@ $('#{{ Input::get('focus') }}').focus(); @endif + // Ensure terms is checked for sign up form + @if (Auth::check() && !Auth::user()->registered) + setSignupEnabled(false); + $("#terms_checkbox").change(function() { + setSignupEnabled(this.checked); + }); + @endif + }); @@ -527,11 +541,37 @@ {!! Former::text('go_pro') !!}

- {!! Former::text('new_first_name')->label(trans('texts.first_name')) !!} - {!! Former::text('new_last_name')->label(trans('texts.last_name')) !!} - {!! Former::text('new_email')->label(trans('texts.email')) !!} - {!! Former::password('new_password')->label(trans('texts.password')) !!} - {!! Former::checkbox('terms_checkbox')->label(' ')->text(trans('texts.agree_to_terms', ['terms' => ''.trans('texts.terms_of_service').''])) !!} + +
@@ -87,16 +93,23 @@ {!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown'")->onchange('onPartialChange()') ->rel('tooltip')->data_toggle('tooltip')->data_placement('bottom')->title(trans('texts.partial_value')) !!}
- @if ($entityType == ENTITY_INVOICE) -
- {!! Former::select('frequency_id')->options($frequencies)->data_bind("value: frequency_id") - ->appendIcon('question-sign')->addGroupClass('frequency_id') !!} - {!! Former::text('start_date')->data_bind("datePicker: start_date, valueUpdate: 'afterkeydown'") - ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('start_date') !!} - {!! Former::text('end_date')->data_bind("datePicker: end_date, valueUpdate: 'afterkeydown'") - ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('end_date') !!} -
- @if ($invoice && $invoice->recurring_invoice) + @if ($entityType == ENTITY_INVOICE) +
+ {!! Former::select('frequency_id')->options($frequencies)->data_bind("value: frequency_id") + ->appendIcon('question-sign')->addGroupClass('frequency_id') !!} + {!! Former::text('start_date')->data_bind("datePicker: start_date, valueUpdate: 'afterkeydown'") + ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('start_date') !!} + {!! Former::text('end_date')->data_bind("datePicker: end_date, valueUpdate: 'afterkeydown'") + ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('end_date') !!} +
+ @endif + + @if ($account->custom_invoice_text_label1 || $invoice && $account->custom_text_value1) + {!! Former::text('custom_text_value1')->label($account->custom_invoice_text_label1)->data_bind("value: custom_text_value1, valueUpdate: 'afterkeydown'") !!} + @endif + + @if ($entityType == ENTITY_INVOICE) + @if ($invoice && $invoice->recurring_invoice)
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
@@ -127,6 +140,11 @@ Former::select('is_amount_discount')->addOption(trans('texts.discount_percent'), '0') ->addOption(trans('texts.discount_amount'), '1')->data_bind("value: is_amount_discount")->raw() ) !!} + + @if ($account->custom_invoice_text_label2 || $invoice && $account->custom_text_value2) + {!! Former::text('custom_text_value2')->label($account->custom_invoice_text_label2)->data_bind("value: custom_text_value2, valueUpdate: 'afterkeydown'") !!} + @endif +
@@ -614,7 +632,7 @@ }); } - $('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial').change(function() { + $('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial, #custom_text_value1, #custom_text_value2').change(function() { setTimeout(function() { refreshPDF(true); }, 1); @@ -1207,6 +1225,8 @@ self.custom_value2 = ko.observable(0); self.custom_taxes1 = ko.observable(false); self.custom_taxes2 = ko.observable(false); + self.custom_text_value1 = ko.observable(); + self.custom_text_value2 = ko.observable(); self.mapping = { 'client': { @@ -1573,11 +1593,14 @@ self.email = ko.observable(''); self.phone = ko.observable(''); self.send_invoice = ko.observable(false); - self.invitation_link = ko.observable(''); + self.invitation_link = ko.observable(''); + self.invitation_status = ko.observable(''); + self.invitation_viewed = ko.observable(false); + self.email_error = ko.observable(''); if (data) { - ko.mapping.fromJS(data, {}, this); - } + ko.mapping.fromJS(data, {}, this); + } self.displayName = ko.computed(function() { var str = ''; @@ -1598,8 +1621,12 @@ } if (self.email()) { str += self.email() + '
'; - } + } + return str; + }); + self.view_as_recipient = ko.computed(function() { + var str = ''; @if (Utils::isConfirmed()) if (self.invitation_link()) { str += '{{ trans('texts.view_as_recipient') }}'; diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index cf91e9908402..64fdac4f76e4 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -33,7 +33,7 @@ @if (count($paymentTypes) > 1) {!! DropdownButton::success(trans('texts.pay_now'))->withContents($paymentTypes)->large() !!} @else - {!! Button::success(trans('texts.pay_now'))->asLinkTo(URL::to($paymentURL))->large() !!} + {!! link_to(URL::to($paymentURL), trans('texts.pay_now'), ['class' => 'btn btn-success btn-lg']) !!} @endif @else {!! Button::normal('Download PDF')->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!} diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index 34ef39f6e91b..20501e28f173 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -172,8 +172,6 @@ trackEvent('/view_link', track ? track : url); window.open(url, '_blank'); } - -//$('a[rel!=ext]').click(function() { $(window).off('beforeunload') }); diff --git a/resources/views/partials/social_login_buttons.blade.php b/resources/views/partials/social_login_buttons.blade.php new file mode 100644 index 000000000000..d7bd1272e65c --- /dev/null +++ b/resources/views/partials/social_login_buttons.blade.php @@ -0,0 +1,16 @@ +@foreach (App\Services\AuthService::$providers as $provider) + +@endforeach + + \ No newline at end of file diff --git a/resources/views/partials/warn_session.blade.php b/resources/views/partials/warn_session.blade.php index fc0762285621..43a31f471420 100644 --- a/resources/views/partials/warn_session.blade.php +++ b/resources/views/partials/warn_session.blade.php @@ -32,7 +32,7 @@ } $(function() { - if ($('form.warn-on-exit').length > 0) { + if ($('form.warn-on-exit, form.form-signin').length > 0) { startWarnSessionTimeout(); } }); diff --git a/tests/_support/_generated/AcceptanceTesterActions.php b/tests/_support/_generated/AcceptanceTesterActions.php index 3135d9aa0510..2adc19ca2c5a 100644 --- a/tests/_support/_generated/AcceptanceTesterActions.php +++ b/tests/_support/_generated/AcceptanceTesterActions.php @@ -1,4 +1,4 @@ -First + * Second + * Third + * ``` + * + * ```php + * grabMultiple('a'); + * + * // would return ['#first', '#second', '#third'] + * $aLinks = $I->grabMultiple('a', 'href'); + * ?> + * ``` + * + * @param $cssOrXpath + * @param $attribute + * @return string[] * @see \Codeception\Module\WebDriver::grabMultiple() */ public function grabMultiple($cssOrXpath, $attribute = null) { @@ -1640,6 +1661,27 @@ trait AcceptanceTesterActions } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Module\WebDriver::seeNumberOfElementsInDOM() + */ + public function canSeeNumberOfElementsInDOM($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumberOfElementsInDOM', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\WebDriver::seeNumberOfElementsInDOM() + */ + public function seeNumberOfElementsInDOM($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumberOfElementsInDOM', func_get_args())); + } + + /** * [!] Method is generated. Documentation taken from corresponding module. * @@ -2463,34 +2505,7 @@ trait AcceptanceTesterActions /** * [!] Method is generated. Documentation taken from corresponding module. * - * Saves current cookies into named snapshot in order to restore them in other tests - * This is useful to save session state between tests. - * For example, if user needs log in to site for each test this scenario can be executed once - * while other tests can just restore saved cookies. - * - * ``` php - * loadSessionSnapshot('login')) return; - * - * // logging in - * $I->amOnPage('/login'); - * $I->fillField('name', 'jon'); - * $I->fillField('password', '123345'); - * $I->click('Login'); - * - * // saving snapshot - * $I->saveSessionSnapshot('login'); - * } - * ?> - * ``` - * - * @param $name - * @return mixed + * @param string $name * @see \Codeception\Module\WebDriver::saveSessionSnapshot() */ public function saveSessionSnapshot($name) { @@ -2501,11 +2516,8 @@ trait AcceptanceTesterActions /** * [!] Method is generated. Documentation taken from corresponding module. * - * Loads cookies from saved snapshot. - * - * @param $name - * @see saveSessionSnapshot - * @return mixed + * @param string $name + * @return bool * @see \Codeception\Module\WebDriver::loadSessionSnapshot() */ public function loadSessionSnapshot($name) { @@ -2603,13 +2615,13 @@ trait AcceptanceTesterActions * ?> * ``` * - * @param int $num Expected number + * @param int $expectedNumber Expected number * @param string $table Table name * @param array $criteria Search criteria [Optional] * Conditional Assertion: Test won't be stopped on fail * @see \Codeception\Module\Db::seeNumRecords() */ - public function canSeeNumRecords($num, $table, $criteria = null) { + public function canSeeNumRecords($expectedNumber, $table, $criteria = null) { return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumRecords', func_get_args())); } /** @@ -2623,12 +2635,12 @@ trait AcceptanceTesterActions * ?> * ``` * - * @param int $num Expected number + * @param int $expectedNumber Expected number * @param string $table Table name * @param array $criteria Search criteria [Optional] * @see \Codeception\Module\Db::seeNumRecords() */ - public function seeNumRecords($num, $table, $criteria = null) { + public function seeNumRecords($expectedNumber, $table, $criteria = null) { return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumRecords', func_get_args())); }