1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-24 22:40:55 +01:00

Streamline Legalpad signature workflow

Summary:
Generally reduces friction, standardizes, and simplifies this workflow. Particularly, this removes "address" and "phone", which I think we can wait for user demand for.

For logged-in users, we just always use their primary email.

Test Plan: See screenshots.

Reviewers: chad

Reviewed By: chad

Subscribers: epriestley

Differential Revision: https://secure.phabricator.com/D9735
This commit is contained in:
epriestley 2014-06-26 07:16:42 -07:00
parent 3d1e865804
commit 950d3668f9
7 changed files with 184 additions and 163 deletions

View file

@ -7,7 +7,7 @@
return array( return array(
'names' => 'names' =>
array( array(
'core.pkg.css' => 'e428441c', 'core.pkg.css' => '3f0f5da2',
'core.pkg.js' => '8c184823', 'core.pkg.js' => '8c184823',
'darkconsole.pkg.js' => 'df001cab', 'darkconsole.pkg.js' => 'df001cab',
'differential.pkg.css' => '4a93db37', 'differential.pkg.css' => '4a93db37',
@ -20,7 +20,7 @@ return array(
'rsrc/css/aphront/context-bar.css' => '1c3b0529', 'rsrc/css/aphront/context-bar.css' => '1c3b0529',
'rsrc/css/aphront/dark-console.css' => '6378ef3d', 'rsrc/css/aphront/dark-console.css' => '6378ef3d',
'rsrc/css/aphront/dialog-view.css' => '4dbbe3bb', 'rsrc/css/aphront/dialog-view.css' => '4dbbe3bb',
'rsrc/css/aphront/error-view.css' => '9f1d5518', 'rsrc/css/aphront/error-view.css' => '3462dbee',
'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
'rsrc/css/aphront/list-filter-view.css' => '2ae43867', 'rsrc/css/aphront/list-filter-view.css' => '2ae43867',
'rsrc/css/aphront/multi-column.css' => '1b95ab2e', 'rsrc/css/aphront/multi-column.css' => '1b95ab2e',
@ -499,7 +499,7 @@ return array(
'aphront-contextbar-view-css' => '1c3b0529', 'aphront-contextbar-view-css' => '1c3b0529',
'aphront-dark-console-css' => '6378ef3d', 'aphront-dark-console-css' => '6378ef3d',
'aphront-dialog-view-css' => '4dbbe3bb', 'aphront-dialog-view-css' => '4dbbe3bb',
'aphront-error-view-css' => '9f1d5518', 'aphront-error-view-css' => '3462dbee',
'aphront-list-filter-view-css' => '2ae43867', 'aphront-list-filter-view-css' => '2ae43867',
'aphront-multi-column-view-css' => '1b95ab2e', 'aphront-multi-column-view-css' => '1b95ab2e',
'aphront-pager-view-css' => '2e3539af', 'aphront-pager-view-css' => '2e3539af',

View file

@ -860,6 +860,7 @@ phutil_register_library_map(array(
'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php', 'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php', 'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
'LegalpadDocumentCommentController' => 'applications/legalpad/controller/LegalpadDocumentCommentController.php', 'LegalpadDocumentCommentController' => 'applications/legalpad/controller/LegalpadDocumentCommentController.php',
'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php', 'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php', 'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php', 'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
@ -3613,6 +3614,7 @@ phutil_register_library_map(array(
1 => 'PhabricatorMarkupInterface', 1 => 'PhabricatorMarkupInterface',
), ),
'LegalpadDocumentCommentController' => 'LegalpadController', 'LegalpadDocumentCommentController' => 'LegalpadController',
'LegalpadDocumentDoneController' => 'LegalpadController',
'LegalpadDocumentEditController' => 'LegalpadController', 'LegalpadDocumentEditController' => 'LegalpadController',
'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor', 'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
'LegalpadDocumentListController' => 'LegalpadController', 'LegalpadDocumentListController' => 'LegalpadController',

View file

@ -46,6 +46,7 @@ final class PhabricatorApplicationLegalpad extends PhabricatorApplication {
'edit/(?P<id>\d+)/' => 'LegalpadDocumentEditController', 'edit/(?P<id>\d+)/' => 'LegalpadDocumentEditController',
'comment/(?P<id>\d+)/' => 'LegalpadDocumentCommentController', 'comment/(?P<id>\d+)/' => 'LegalpadDocumentCommentController',
'view/(?P<id>\d+)/' => 'LegalpadDocumentManageController', 'view/(?P<id>\d+)/' => 'LegalpadDocumentManageController',
'done/' => 'LegalpadDocumentDoneController',
'verify/(?P<code>[^/]+)/' => 'verify/(?P<code>[^/]+)/' =>
'LegalpadDocumentSignatureVerificationController', 'LegalpadDocumentSignatureVerificationController',
'signatures/(?P<id>\d+)/' => 'LegalpadDocumentSignatureListController', 'signatures/(?P<id>\d+)/' => 'LegalpadDocumentSignatureListController',

View file

@ -0,0 +1,22 @@
<?php
final class LegalpadDocumentDoneController extends LegalpadController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
return $this->newDialog()
->setTitle(pht('Verify Signature'))
->appendParagraph(
pht(
'Thank you for signing this document. Please check your email '.
'to verify your signature and complete the process.'))
->addCancelButton('/', pht('Okay'));
}
}

View file

@ -4,6 +4,10 @@ final class LegalpadDocumentListController extends LegalpadController {
private $queryKey; private $queryKey;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) { public function willProcessRequest(array $data) {
$this->queryKey = idx($data, 'queryKey'); $this->queryKey = idx($data, 'queryKey');
} }

View file

@ -21,145 +21,162 @@ final class LegalpadDocumentSignController extends LegalpadController {
->withIDs(array($this->id)) ->withIDs(array($this->id))
->needDocumentBodies(true) ->needDocumentBodies(true)
->executeOne(); ->executeOne();
if (!$document) { if (!$document) {
return new Aphront404Response(); return new Aphront404Response();
} }
$signer_phid = null; $signer_phid = null;
$signature = null;
$signature_data = array(); $signature_data = array();
if ($viewer->isLoggedIn()) { if ($viewer->isLoggedIn()) {
$signer_phid = $viewer->getPHID(); $signer_phid = $viewer->getPHID();
$signature_data = array( $signature_data = array(
'email' => $viewer->loadPrimaryEmailAddress()); 'name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
);
} else if ($request->isFormPost()) { } else if ($request->isFormPost()) {
$email = new PhutilEmailAddress($request->getStr('email')); $email = new PhutilEmailAddress($request->getStr('email'));
$email_obj = id(new PhabricatorUserEmail()) if (strlen($email->getDomainName())) {
->loadOneWhere('address = %s', $email->getAddress()); $email_obj = id(new PhabricatorUserEmail())
if ($email_obj) { ->loadOneWhere('address = %s', $email->getAddress());
return $this->signInResponse(); if ($email_obj) {
return $this->signInResponse();
}
$external_account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withAccountTypes(array('email'))
->withAccountDomains(array($email->getDomainName()))
->withAccountIDs(array($email->getAddress()))
->loadOneOrCreate();
if ($external_account->getUserPHID()) {
return $this->signInResponse();
}
$signer_phid = $external_account->getPHID();
} }
$external_account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withAccountTypes(array('email'))
->withAccountDomains(array($email->getDomainName()))
->withAccountIDs(array($email->getAddress()))
->loadOneOrCreate();
if ($external_account->getUserPHID()) {
return $this->signInResponse();
}
$signer_phid = $external_account->getPHID();
} }
$signature = null;
if ($signer_phid) { if ($signer_phid) {
// TODO: This is odd and should probably be adjusted after grey/external
// accounts work better, but use the omnipotent viewer to check for a
// signature so we can pick up anonymous/grey signatures.
$signature = id(new LegalpadDocumentSignatureQuery()) $signature = id(new LegalpadDocumentSignatureQuery())
->setViewer($viewer) ->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs(array($document->getPHID())) ->withDocumentPHIDs(array($document->getPHID()))
->withSignerPHIDs(array($signer_phid)) ->withSignerPHIDs(array($signer_phid))
->withDocumentVersions(array($document->getVersions())) ->withDocumentVersions(array($document->getVersions()))
->executeOne(); ->executeOne();
if ($signature && !$viewer->isLoggedIn()) {
return $this->newDialog()
->setTitle(pht('Already Signed'))
->appendParagraph(pht('You have already signed this document!'))
->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
}
} }
$signed_status = null;
if (!$signature) { if (!$signature) {
$has_signed = false; $has_signed = false;
$error_view = null;
$signature = id(new LegalpadDocumentSignature()) $signature = id(new LegalpadDocumentSignature())
->setSignerPHID($signer_phid) ->setSignerPHID($signer_phid)
->setDocumentPHID($document->getPHID()) ->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions()) ->setDocumentVersion($document->getVersions())
->setSignatureData($signature_data); ->setSignatureData($signature_data);
// If the user is logged in, show a notice that they haven't signed.
// If they aren't logged in, we can't be as sure, so don't show anything.
if ($viewer->isLoggedIn()) {
$signed_status = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setErrors(
array(
pht('You have not signed this document yet.'),
));
}
} else { } else {
$has_signed = true; $has_signed = true;
if ($signature->isVerified()) {
$title = pht('Already Signed');
$body = $this->getVerifiedSignatureBlurb();
} else {
$title = pht('Already Signed but...');
$body = $this->getUnverifiedSignatureBlurb();
}
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle($title)
->appendChild($body);
$signature_data = $signature->getSignatureData(); $signature_data = $signature->getSignatureData();
// In this case, we know they've signed.
$signed_at = $signature->getDateCreated();
$signed_status = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setErrors(
array(
pht(
'You signed this document on %s.',
phabricator_datetime($signed_at, $viewer)),
));
} }
$e_name = true; $e_name = true;
$e_email = true; $e_email = true;
$e_address_1 = true; $e_agree = null;
$errors = array(); $errors = array();
if ($request->isFormPost() && !$has_signed) { if ($request->isFormPost() && !$has_signed) {
$name = $request->getStr('name'); $name = $request->getStr('name');
$email = $request->getStr('email');
$address_1 = $request->getStr('address_1');
$address_2 = $request->getStr('address_2');
$phone = $request->getStr('phone');
$agree = $request->getExists('agree'); $agree = $request->getExists('agree');
if (!$name) { if (!strlen($name)) {
$e_name = pht('Required'); $e_name = pht('Required');
$errors[] = pht('Name field is required.'); $errors[] = pht('Name field is required.');
} else {
$e_name = null;
} }
$signature_data['name'] = $name; $signature_data['name'] = $name;
$addr_obj = null; if ($viewer->isLoggedIn()) {
if (!$email) { $email = $viewer->loadPrimaryEmailAddress();
$e_email = pht('Required');
$errors[] = pht('Email field is required.');
} else { } else {
$addr_obj = new PhutilEmailAddress($email); $email = $request->getStr('email');
$domain = $addr_obj->getDomainName();
if (!$domain) { $addr_obj = null;
$e_email = pht('Invalid'); if (!strlen($email)) {
$errors[] = pht('A valid email is required.'); $e_email = pht('Required');
$errors[] = pht('Email field is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$e_email = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$e_email = null;
}
} }
} }
$signature_data['email'] = $email; $signature_data['email'] = $email;
if (!$address_1) {
$e_address_1 = pht('Required');
$errors[] = pht('Address line 1 field is required.');
}
$signature_data['address_1'] = $address_1;
$signature_data['address_2'] = $address_2;
$signature_data['phone'] = $phone;
$signature->setSignatureData($signature_data); $signature->setSignatureData($signature_data);
if (!$agree) { if (!$agree) {
$errors[] = pht( $errors[] = pht(
'You must check "I agree to the terms laid forth above."'); 'You must check "I agree to the terms laid forth above."');
$e_agree = pht('Required');
} }
$verified = LegalpadDocumentSignature::UNVERIFIED; if ($viewer->isLoggedIn()) {
if ($viewer->isLoggedIn() && $addr_obj) { $verified = LegalpadDocumentSignature::VERIFIED;
$email_obj = id(new PhabricatorUserEmail()) } else {
->loadOneWhere('address = %s', $addr_obj->getAddress()); $verified = LegalpadDocumentSignature::UNVERIFIED;
if ($email_obj && $email_obj->getUserPHID() == $viewer->getPHID()) {
$verified = LegalpadDocumentSignature::VERIFIED;
}
} }
$signature->setVerified($verified); $signature->setVerified($verified);
if (!$errors) { if (!$errors) {
$signature->save(); $signature->save();
$has_signed = true;
if ($signature->isVerified()) { // If the viewer is logged in, send them to the document page, which
$body = $this->getVerifiedSignatureBlurb(); // will show that they have signed the document. Otherwise, send them
// to a completion page.
if ($viewer->isLoggedIn()) {
$next_uri = '/'.$document->getMonogram();
} else { } else {
$body = $this->getUnverifiedSignatureBlurb(); $next_uri = $this->getApplicationURI('done/');
$this->sendVerifySignatureEmail(
$document,
$signature);
} }
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE) return id(new AphrontRedirectResponse())->setURI($next_uri);
->setTitle(pht('Signature Successful'))
->appendChild($body);
} else {
$error_view = id(new AphrontErrorView())
->setTitle(pht('Error in submission.'))
->setErrors($errors);
} }
} }
@ -171,6 +188,10 @@ final class LegalpadDocumentSignController extends LegalpadController {
LegalpadDocumentBody::MARKUP_FIELD_TEXT); LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$engine->process(); $engine->process();
$document_markup = $engine->getOutput(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$title = $document_body->getTitle(); $title = $document_body->getTitle();
$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/'); $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
@ -193,25 +214,36 @@ final class LegalpadDocumentSignController extends LegalpadController {
->setDisabled(!$can_edit) ->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)); ->setWorkflow(!$can_edit));
$signature_form = $this->buildSignatureForm( $content = id(new PHUIDocumentView())
$document_body, ->addClass('legalpad')
$signature, ->setHeader($header)
$has_signed, ->appendChild(
$e_name, array(
$e_email, $signed_status,
$e_address_1); $document_markup,
));
$content = $this->buildDocument( if (!$has_signed) {
$header, $error_view = null;
$engine, if ($errors) {
$document_body); $error_view = id(new AphrontErrorView())
->setErrors($errors);
}
$content->appendChild( $signature_form = $this->buildSignatureForm(
array( $document_body,
id(new PHUIHeaderView())->setHeader(pht('Agree and Sign Document')), $signature,
$error_view, $e_name,
$signature_form, $e_email,
)); $e_agree);
$content->appendChild(
array(
id(new PHUIHeaderView())->setHeader(pht('Agree and Sign Document')),
$error_view,
$signature_form,
));
}
$crumbs = $this->buildApplicationCrumbs(); $crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($document->getMonogram()); $crumbs->addTextCrumb($document->getMonogram());
@ -227,35 +259,16 @@ final class LegalpadDocumentSignController extends LegalpadController {
)); ));
} }
private function buildDocument(
PHUIHeaderView $header,
PhabricatorMarkupEngine $engine,
LegalpadDocumentBody $body) {
return id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->appendChild($engine->getOutput(
$body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT));
}
private function buildSignatureForm( private function buildSignatureForm(
LegalpadDocumentBody $body, LegalpadDocumentBody $body,
LegalpadDocumentSignature $signature, LegalpadDocumentSignature $signature,
$has_signed = false, $e_name,
$e_name = true, $e_email,
$e_email = true, $e_agree) {
$e_address_1 = true) {
$viewer = $this->getRequest()->getUser(); $viewer = $this->getRequest()->getUser();
if ($has_signed) {
$instructions = pht('Thank you for signing and agreeing.');
} else {
$instructions = pht('Please enter the following information.');
}
$data = $signature->getSignatureData(); $data = $signature->getSignatureData();
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->appendChild( ->appendChild(
@ -263,61 +276,34 @@ final class LegalpadDocumentSignController extends LegalpadController {
->setLabel(pht('Name')) ->setLabel(pht('Name'))
->setValue(idx($data, 'name', '')) ->setValue(idx($data, 'name', ''))
->setName('name') ->setName('name')
->setError($e_name) ->setError($e_name));
->setDisabled($has_signed))
->appendChild( if (!$viewer->isLoggedIn()) {
$form->appendChild(
id(new AphrontFormTextControl()) id(new AphrontFormTextControl())
->setLabel(pht('Email')) ->setLabel(pht('Email'))
->setValue(idx($data, 'email', '')) ->setValue(idx($data, 'email', ''))
->setName('email') ->setName('email')
->setError($e_email) ->setError($e_email));
->setDisabled($has_signed)) }
->appendChild(
id(new AphrontFormTextControl()) $form
->setLabel(pht('Address line 1'))
->setValue(idx($data, 'address_1', ''))
->setName('address_1')
->setError($e_address_1)
->setDisabled($has_signed))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Address line 2'))
->setValue(idx($data, 'address_2', ''))
->setName('address_2')
->setDisabled($has_signed))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Phone'))
->setValue(idx($data, 'phone', ''))
->setName('phone')
->setDisabled($has_signed))
->appendChild( ->appendChild(
id(new AphrontFormCheckboxControl()) id(new AphrontFormCheckboxControl())
->addCheckbox( ->setError($e_agree)
'agree', ->addCheckbox(
'agree', 'agree',
pht('I agree to the terms laid forth above.'), 'agree',
$has_signed) pht('I agree to the terms laid forth above.'),
->setDisabled($has_signed)) false))
->appendChild( ->appendChild(
id(new AphrontFormSubmitControl()) id(new AphrontFormSubmitControl())
->setValue(pht('Sign and Agree')) ->setValue(pht('Sign Document'))
->setDisabled($has_signed) ->addCancelButton($this->getApplicationURI()));
->addCancelButton($this->getApplicationURI()));
return $form; return $form;
} }
private function getVerifiedSignatureBlurb() {
return pht('Thank you for signing and agreeing.');
}
private function getUnverifiedSignatureBlurb() {
return pht('Thank you for signing and agreeing. However, you must '.
'verify your email address. Please check your email '.
'and follow the instructions.');
}
private function sendVerifySignatureEmail( private function sendVerifySignatureEmail(
LegalpadDocument $doc, LegalpadDocument $doc,
LegalpadDocumentSignature $signature) { LegalpadDocumentSignature $signature) {

View file

@ -77,6 +77,12 @@ h1.aphront-error-view-head {
background-color: #fff; background-color: #fff;
} }
.legalpad .aphront-error-view {
margin: 0;
border-width: 0 0 1px 0;
border-bottom: 1px solid {$lightblueborder};
}
.aphront-dialog-body .aphront-error-view { .aphront-dialog-body .aphront-error-view {
margin: -16px -16px 16px -16px; margin: -16px -16px 16px -16px;
border-width: 0 0 1px 0; border-width: 0 0 1px 0;