Skip to content

Commit 0a31411

Browse files
authored
Merge pull request #34 from clicksign/feature/VOP-552
[BACK] Criar exemplo de widget para assinatura encorporada de envelopes
2 parents 50eeeeb + 4e79b03 commit 0a31411

File tree

3 files changed

+562
-0
lines changed

3 files changed

+562
-0
lines changed

v3/embedded_signature/index.html

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Assinatura Encorporada Moderna</title>
7+
<link href="../style.css" rel="stylesheet"/>
8+
<script src="../noWidget.js"></script>
9+
</head>
10+
11+
<body>
12+
<div class="container">
13+
<aside class="card">
14+
<h2 class="title">⚙️ Configurações</h2>
15+
<div class="form">
16+
<div class="field">
17+
<label for="key">Chave do Signatário</label>
18+
<input type="text" id="key" name="key" required="true" />
19+
</div>
20+
<div class="field">
21+
<label for="request_enviroment">Environment</label>
22+
<select id="request_enviroment">
23+
<option value="sandbox" selected>Sandbox</option>
24+
<option value="production">Produção</option>
25+
</select>
26+
</div>
27+
<div class="action">
28+
<button id="setButton" type="submit" class="button" onclick="init()">
29+
Carregar
30+
</button>
31+
</div>
32+
</div>
33+
</aside>
34+
35+
<main class="card">
36+
<h2 class="title">🙋 Preencha seus dados</h2>
37+
<div id="notification"></div>
38+
<form id="form" action="#" onsubmit="return false" class="form">
39+
<div class="signer-info">
40+
<div class="grid">
41+
<div class="field">
42+
<label for="name">Nome</label>
43+
<input type="text" id="name" name="name" />
44+
</div>
45+
<div class="field" id="cpf-field-container">
46+
<label for="cpf">CPF</label>
47+
<input type="text" id="cpf" name="cpf" />
48+
</div>
49+
</div>
50+
<div class="field" id="birthdate-field-container">
51+
<label for="birthdate">Data de nascimento</label>
52+
<input type="date" id="birthdate" name="birthdate" />
53+
</div>
54+
</div>
55+
56+
<div id="widget"></div>
57+
58+
<div class="action">
59+
<button type="submit" class="button large" id="submitButton" disabled="disabled">
60+
Clique para assinar
61+
</button>
62+
</div>
63+
</form>
64+
</main>
65+
</div>
66+
67+
<script type="module">
68+
let const_value = 20
69+
if (typeof import.meta.env !== 'undefined') {
70+
const_value = Number(import.meta.env?.VITE_ENVIRONMENTS_COUNT);
71+
}
72+
window.ENVIRONMENTS_COUNT = const_value;
73+
</script>
74+
75+
<script type="text/javascript">
76+
let environment_element = document.getElementById('request_enviroment')
77+
78+
window.onload = function() {
79+
for (let index = 1; index <= ENVIRONMENTS_COUNT; index++) {
80+
let option = document.createElement('option');
81+
option.text = `Staging ${index}`;
82+
option.value = `staging${index}`;
83+
environment_element.appendChild(option);
84+
}
85+
}
86+
</script>
87+
88+
<script type="text/javascript">
89+
function init(){
90+
const key = document.getElementById('key').value
91+
const enviroment = document.getElementById('request_enviroment').value
92+
const submitButton = document.getElementById('submitButton')
93+
94+
const nw = new noWidget(key, enviroment)
95+
96+
nw.injectLoader('<div class="loader"><div></div><div></div><div></div><div></div></div>')
97+
98+
nw.attach('click', 'submitButton', () => {
99+
document.body.classList.remove('error');
100+
document.body.classList.add('loading');
101+
const data = {
102+
"signature": {
103+
"signer": {
104+
"name": document.getElementById("name").value,
105+
"birthday": document.getElementById("birthdate").value,
106+
"documentation": document.getElementById("cpf").value
107+
},
108+
"document_keys": nw.getDocumentKeys()
109+
}
110+
}
111+
112+
const notification = document.getElementById('notification');
113+
114+
nw.sign(data).then(() => {
115+
document.body.classList.remove('loading');
116+
document.body.classList.add('signed');
117+
submitButton.disabled = true;
118+
notification.innerHTML = "Assinatura realizada com sucesso!";
119+
notification.classList.add('success');
120+
notification.classList.remove('error');
121+
console.log('Assinatura realizada com sucesso!')
122+
}).catch((error) => {
123+
document.body.classList.remove('loading');
124+
document.body.classList.add('error');
125+
submitButton.disabled = false;
126+
notification.innerHTML = `Erro ao assinar o documento. Confira log`;
127+
notification.classList.add('error');
128+
notification.classList.remove('success');
129+
console.log(error.code, error.message)
130+
})
131+
})
132+
133+
nw.mount({'containerId':'widget'})
134+
135+
submitButton.removeAttribute('disabled')
136+
}
137+
</script>
138+
</body>
139+
</html>

v3/noWidget.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
class noWidget {
2+
#origin = window.location.hostname;
3+
#baseUrl;
4+
#loader = 'Carregando Widget...';
5+
#mounted = false;
6+
#wrapper = null;
7+
#requiredFields = {};
8+
#documentKeys = [];
9+
10+
constructor(uuid, env = 'production', options = { domain: '.clicksign.com', show_files: false, pdf: false }) {
11+
let subDomain = env;
12+
let baseDomain = options.domain;
13+
14+
if (env === 'production') {
15+
subDomain = 'app';
16+
} else if (/staging\d+/.test(env)) {
17+
subDomain = env.match(/\d+/)[0];
18+
baseDomain = '.clicksign.dev';
19+
}
20+
21+
this.#baseUrl = `https://${subDomain}${baseDomain}/notarial/embedded_signature/signatures/${uuid}`;
22+
this.options = options;
23+
}
24+
25+
mount({ containerId }) {
26+
this.#wrapper = document.getElementById(containerId);
27+
28+
if (this.#wrapper === null) {
29+
this.#mounted = false;
30+
throw new Error("It's not possible to render widget.");
31+
}
32+
33+
this.#wrapper.innerHTML = this.#loader;
34+
this.#getDocuments(this.#wrapper);
35+
this.#mounted = true;
36+
}
37+
38+
attach(event, elID, method) {
39+
const el = document.getElementById(elID);
40+
el.addEventListener(event, method);
41+
}
42+
43+
async sign(info) {
44+
if (!this.#mounted) { throw new Error('Widget not mounted.'); }
45+
if (this.#requiredFields) { this.#validateFields(info); }
46+
47+
try {
48+
const response = await fetch(`${this.#baseUrl}/sign`, {
49+
method: 'POST',
50+
body: JSON.stringify(info),
51+
headers: {
52+
'Origin': this.#origin,
53+
'Content-Type': 'application/json'
54+
}
55+
});
56+
57+
const responseText = await response.text();
58+
if (response.status === 200 && !responseText) {
59+
return { status: 'success' };
60+
}
61+
62+
if (!responseText) {
63+
throw new Error('Empty response from server');
64+
}
65+
66+
let data = JSON.parse(responseText);
67+
68+
if (data.status === 'error') {
69+
throw data.error;
70+
}
71+
72+
return data;
73+
} catch (error) {
74+
console.error("Verifique os parâmetros da requisição e tente novamente.", error);
75+
throw error;
76+
}
77+
}
78+
79+
injectLoader(el) {
80+
this.#loader = el;
81+
}
82+
83+
getDocumentKeys() {
84+
return this.#documentKeys;
85+
}
86+
87+
#validateFields(info) {
88+
const { signature } = info;
89+
if (!signature) { throw new Error("Object signature is required."); }
90+
if (!signature.signer) { throw new Error("Object signature.signer is required."); }
91+
92+
const requiredAttributes = Object.keys(this.#requiredFields)
93+
.filter((field) => this.#requiredFields[field]);
94+
95+
if (!(requiredAttributes.every((attr) => signature.signer[attr]))) {
96+
throw new Error(`Can't sign without required fields.️ ${requiredAttributes}`);
97+
}
98+
}
99+
100+
#createDocumentLink(doc, strMatch) {
101+
let filename = this.options.pdf ? doc.pdf_name : doc.name;
102+
let download_url = this.options.pdf ? doc.pdf_url : doc.original_url;
103+
104+
let docLink = document.createElement('a');
105+
docLink.innerHTML = strMatch && `${strMatch[1]}`;
106+
docLink.className = 'cs-acceptance-preview';
107+
docLink.setAttribute('download', 'true');
108+
docLink.textContent = filename;
109+
docLink.href = download_url;
110+
111+
return docLink;
112+
}
113+
114+
async #getDocuments(wrapper) {
115+
try {
116+
const response = await fetch(`${this.#baseUrl}/documents`, {
117+
headers: {
118+
'Origin': this.#origin,
119+
'Content-Type': 'application/json',
120+
},
121+
});
122+
123+
const responseText = await response.text();
124+
if (!responseText) {
125+
throw new Error('Empty response from server');
126+
}
127+
128+
let data;
129+
try {
130+
data = JSON.parse(responseText);
131+
} catch (e) {
132+
console.error('Failed to parse JSON:', responseText);
133+
throw new Error('Invalid JSON response from server');
134+
}
135+
136+
const { documents, error, required_fields } = data;
137+
138+
if (!documents) { throw new Error(error.message); }
139+
140+
this.#requiredFields = required_fields;
141+
this.#documentKeys = documents.map(doc => doc.key);
142+
wrapper.innerHTML = '';
143+
144+
this.#hideNonRequiredFields();
145+
146+
const regex = /\(\((.[^\)\)]*)\)\)/g;
147+
148+
documents.forEach((doc) => {
149+
const docLink = this.#createDocumentLink(doc);
150+
wrapper.appendChild(docLink);
151+
});
152+
} catch (error) {
153+
console.error('Failed to get content:', error);
154+
wrapper.innerHTML = 'Falha ao carregar documentos.';
155+
}
156+
}
157+
158+
#hideNonRequiredFields() {
159+
const fields = {
160+
name: document.getElementById('name').closest('.field'),
161+
documentation: document.getElementById('cpf-field-container'),
162+
birthday: document.getElementById('birthdate-field-container')
163+
};
164+
165+
for (const fieldName in fields) {
166+
if (fields.hasOwnProperty(fieldName)) {
167+
const fieldElement = fields[fieldName];
168+
if (fieldElement && !this.#requiredFields[fieldName]) {
169+
fieldElement.classList.add('hidden-field');
170+
} else if (fieldElement) {
171+
fieldElement.classList.remove('hidden-field');
172+
}
173+
}
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)