implemented amplitude metrics (#1141)

This commit is contained in:
Danny Coates 2019-02-12 11:50:06 -08:00 committed by GitHub
parent 1a483cad55
commit 9b37e92a81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 774 additions and 528 deletions

View file

@ -1,4 +1,4 @@
/* global LIMITS */
/* global LIMITS DEFAULTS */
import { blobStream, concatStream } from './streams';
function isDupe(newFile, array) {
@ -17,6 +17,9 @@ function isDupe(newFile, array) {
export default class Archive {
constructor(files = []) {
this.files = Array.from(files);
this.timeLimit = DEFAULTS.EXPIRE_SECONDS;
this.dlimit = 1;
this.password = null;
}
get name() {
@ -73,5 +76,8 @@ export default class Archive {
clear() {
this.files = [];
this.dlimit = 1;
this.timeLimit = DEFAULTS.EXPIRE_SECONDS;
this.password = null;
}
}

View file

@ -1,4 +1,4 @@
/* global DEFAULTS LIMITS */
/* global LIMITS */
import FileSender from './fileSender';
import FileReceiver from './fileReceiver';
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
@ -50,37 +50,27 @@ export default function(state, emitter) {
emitter.on('logout', () => {
state.user.logout();
state.timeLimit = DEFAULTS.EXPIRE_SECONDS;
state.downloadCount = 1;
metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/');
});
emitter.on('changeLimit', async ({ file, value }) => {
const ok = await file.changeLimit(value, state.user);
if (!ok) {
return;
}
state.storage.writeFile(file);
metrics.changedDownloadLimit(file);
});
emitter.on('removeUpload', file => {
state.archive.remove(file);
render();
});
emitter.on('delete', async ({ file, location }) => {
emitter.on('delete', async ownedFile => {
try {
metrics.deletedUpload({
size: file.size,
time: file.time,
speed: file.speed,
type: file.type,
ttl: file.expiresAt - Date.now(),
size: ownedFile.size,
time: ownedFile.time,
speed: ownedFile.speed,
type: ownedFile.type,
ttl: ownedFile.expiresAt - Date.now(),
location
});
state.storage.remove(file.id);
await file.del();
state.storage.remove(ownedFile.id);
await ownedFile.del();
} catch (e) {
state.raven.captureException(e);
}
@ -100,20 +90,35 @@ export default function(state, emitter) {
state.archive.addFiles(files, maxSize);
} catch (e) {
if (e.message === 'fileTooBig' && maxSize < LIMITS.MAX_FILE_SIZE) {
state.modal = signupDialog();
} else {
state.modal = okDialog(
state.translate(e.message, {
size: bytes(maxSize),
count: LIMITS.MAX_FILES_PER_ARCHIVE
})
);
return emitter.emit('signup-cta', 'size');
}
state.modal = okDialog(
state.translate(e.message, {
size: bytes(maxSize),
count: LIMITS.MAX_FILES_PER_ARCHIVE
})
);
}
render();
});
emitter.on('upload', async ({ type, dlimit, password }) => {
emitter.on('signup-cta', source => {
state.modal = signupDialog(source);
render();
});
emitter.on('authenticate', async (code, oauthState) => {
try {
await state.user.finishLogin(code, oauthState);
await state.user.syncFileList();
emitter.emit('replaceState', '/');
} catch (e) {
emitter.emit('replaceState', '/error');
setTimeout(render);
}
});
emitter.on('upload', async () => {
if (state.storage.files.length >= LIMITS.MAX_ARCHIVES_PER_USER) {
state.modal = okDialog(
state.translate('tooManyArchives', {
@ -122,8 +127,7 @@ export default function(state, emitter) {
);
return render();
}
const size = state.archive.size;
if (!state.timeLimit) state.timeLimit = DEFAULTS.EXPIRE_SECONDS;
const archive = state.archive;
const sender = new FileSender();
sender.on('progress', updateProgress);
@ -135,41 +139,38 @@ export default function(state, emitter) {
const links = openLinksInNewTab();
await delay(200);
const start = Date.now();
try {
metrics.startedUpload({ size, type });
const ownedFile = await sender.upload(
state.archive,
state.timeLimit,
dlimit,
state.user.bearerToken
);
ownedFile.type = type;
const ownedFile = await sender.upload(archive, state.user.bearerToken);
state.storage.totalUploads += 1;
metrics.completedUpload(ownedFile);
const duration = Date.now() - start;
metrics.completedUpload(archive, duration);
state.storage.addFile(ownedFile);
// TODO integrate password into /upload request
if (password) {
emitter.emit('password', { password, file: ownedFile });
if (archive.password) {
emitter.emit('password', {
password: archive.password,
file: ownedFile
});
}
state.modal = copyDialog(ownedFile.name, ownedFile.url);
} catch (err) {
if (err.message === '0') {
//cancelled. do nothing
metrics.cancelledUpload({ size, type });
const duration = Date.now() - start;
metrics.cancelledUpload(archive, duration);
render();
} else {
// eslint-disable-next-line no-console
console.error(err);
state.raven.captureException(err);
metrics.stoppedUpload({ size, type, err });
metrics.stoppedUpload(archive);
emitter.emit('pushState', '/error');
}
} finally {
openLinksInNewTab(links, false);
state.archive.clear();
state.password = '';
archive.clear();
state.uploading = false;
state.transfer = null;
await state.user.syncFileList();
@ -183,7 +184,6 @@ export default function(state, emitter) {
render();
await file.setPassword(password);
state.storage.writeFile(file);
metrics.addedPassword({ size: file.size });
await delay(1000);
} catch (err) {
// eslint-disable-next-line no-console
@ -220,18 +220,20 @@ export default function(state, emitter) {
state.transfer.on('complete', render);
const links = openLinksInNewTab();
const size = file.size;
const start = Date.now();
try {
const start = Date.now();
metrics.startedDownload({ size: file.size, ttl: file.ttl });
const dl = state.transfer.download({
stream: state.capabilities.streamDownload
});
render();
await dl;
const time = Date.now() - start;
const speed = size / (time / 1000);
state.storage.totalDownloads += 1;
metrics.completedDownload({ size, time, speed });
const duration = Date.now() - start;
metrics.completedDownload({
size,
duration,
password_protected: file.requiresPassword
});
} catch (err) {
if (err.message === '0') {
// download cancelled
@ -239,12 +241,16 @@ export default function(state, emitter) {
render();
} else {
// eslint-disable-next-line no-console
console.error(err);
state.transfer = null;
const location = err.message === '404' ? '/404' : '/error';
if (location === '/error') {
state.raven.captureException(err);
metrics.stoppedDownload({ size, err });
const duration = Date.now() - start;
metrics.stoppedDownload({
size,
duration,
password_protected: file.requiresPassword
});
}
emitter.emit('pushState', location);
}
@ -253,9 +259,9 @@ export default function(state, emitter) {
}
});
emitter.on('copy', ({ url, location }) => {
emitter.on('copy', ({ url }) => {
copyToClipboard(url);
metrics.copiedLink({ location });
// metrics.copiedLink({ location });
});
setInterval(() => {

View file

@ -1,4 +1,3 @@
/* global DEFAULTS */
import Nanobus from 'nanobus';
import OwnedFile from './ownedFile';
import Keychain from './keychain';
@ -42,29 +41,24 @@ export default class FileSender extends Nanobus {
}
}
async upload(
file,
timeLimit = DEFAULTS.EXPIRE_SECONDS,
dlimit = 1,
bearerToken
) {
async upload(archive, bearerToken) {
const start = Date.now();
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'encryptingFile';
this.emit('encrypting');
const totalSize = encryptedSize(file.size);
const encStream = await this.keychain.encryptStream(file.stream);
const metadata = await this.keychain.encryptMetadata(file);
const totalSize = encryptedSize(archive.size);
const encStream = await this.keychain.encryptStream(archive.stream);
const metadata = await this.keychain.encryptMetadata(archive);
const authKeyB64 = await this.keychain.authKeyB64();
this.uploadRequest = uploadWs(
encStream,
metadata,
authKeyB64,
timeLimit,
dlimit,
archive.timeLimit,
archive.dlimit,
bearerToken,
p => {
this.progress = [p, totalSize];
@ -88,18 +82,18 @@ export default class FileSender extends Nanobus {
const ownedFile = new OwnedFile({
id: result.id,
url: `${result.url}#${secretKey}`,
name: file.name,
size: file.size,
manifest: file.manifest,
name: archive.name,
size: archive.size,
manifest: archive.manifest,
time: time,
speed: file.size / (time / 1000),
speed: archive.size / (time / 1000),
createdAt: Date.now(),
expiresAt: Date.now() + timeLimit * 1000,
expiresAt: Date.now() + archive.timeLimit * 1000,
secretKey: secretKey,
nonce: this.keychain.nonce,
ownerToken: result.ownerToken,
dlimit,
timeLimit: timeLimit
dlimit: archive.dlimit,
timeLimit: archive.timeLimit
});
return ownedFile;

View file

@ -1,296 +1,172 @@
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
import storage from './storage';
let hasLocalStorage = false;
try {
hasLocalStorage = typeof localStorage !== 'undefined';
} catch (e) {
// when disabled, any mention of localStorage throws an error
}
const analytics = new testPilotGA({
an: 'Firefox Send',
ds: 'web',
tid: window.GOOGLE_ANALYTICS_ID
});
import { platform } from './utils';
let appState = null;
let experiment = null;
// let experiment = null;
const HOUR = 1000 * 60 * 60;
const events = [];
let session_id = Date.now();
const lang = document.querySelector('html').lang;
export default function initialize(state, emitter) {
appState = state;
if (!appState.user.firstAction) {
appState.user.firstAction = appState.route === '/' ? 'upload' : 'download';
}
emitter.on('DOMContentLoaded', () => {
addExitHandlers();
experiment = storage.enrolled[0];
sendEvent(category(), 'visit', {
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads
// experiment = storage.enrolled[0];
addEvent('client_visit', {
entrypoint: appState.route === '/' ? 'upload' : 'download'
});
});
emitter.on('exit', exitEvent);
emitter.on('experiment', experimentEvent);
window.addEventListener('unload', submitEvents);
}
function category() {
switch (appState.route) {
case '/':
case '/share/:id':
return 'sender';
case '/download/:id/:key':
case '/download/:id':
case '/completed':
return 'recipient';
default:
return 'other';
}
function sizeOrder(n) {
return Math.floor(Math.log10(n));
}
function sendEvent() {
const args = Array.from(arguments);
if (experiment && args[2]) {
args[2].xid = experiment[0];
args[2].xvar = experiment[1];
function submitEvents() {
if (navigator.doNotTrack === '1') {
return;
}
return (
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
const data = new Blob(
[
JSON.stringify({
now: Date.now(),
session_id,
lang,
platform: platform(),
events
})
],
{ type: 'application/json' }
);
}
function urlToMetric(url) {
switch (url) {
case 'https://www.mozilla.org/':
return 'mozilla';
case 'https://www.mozilla.org/about/legal':
return 'legal';
case 'https://testpilot.firefox.com/about':
return 'about';
case 'https://testpilot.firefox.com/privacy':
return 'privacy';
case 'https://testpilot.firefox.com/terms':
return 'terms';
case 'https://www.mozilla.org/privacy/websites/#cookies':
return 'cookies';
case 'https://github.com/mozilla/send':
return 'github';
case 'https://twitter.com/FxTestPilot':
return 'twitter';
case 'https://www.mozilla.org/firefox/new/?scene=2':
return 'download-firefox';
case 'https://qsurvey.mozilla.com/s3/txp-firefox-send':
return 'survey';
case 'https://testpilot.firefox.com/':
case 'https://testpilot.firefox.com/experiments/send':
return 'testpilot';
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
return 'promo';
default:
return 'other';
events.splice(0);
if (!navigator.sendBeacon) {
return;
}
navigator.sendBeacon('/api/metrics', data);
}
function setReferrer(state) {
if (category() === 'sender') {
if (state) {
storage.referrer = `${state}-upload`;
}
} else if (category() === 'recipient') {
if (state) {
storage.referrer = `${state}-download`;
async function addEvent(event_type, event_properties) {
const user_id = await appState.user.metricId();
const device_id = await appState.user.deviceId();
events.push({
device_id,
event_properties,
event_type,
time: Date.now(),
user_id,
user_properties: {
anonymous: !appState.user.loggedIn,
first_action: appState.user.firstAction,
active_count: storage.files.length
}
});
if (events.length === 25) {
submitEvents();
}
}
function externalReferrer() {
if (/^https:\/\/testpilot\.firefox\.com/.test(document.referrer)) {
return 'testpilot';
}
return 'external';
}
function takeReferrer() {
const referrer = storage.referrer || externalReferrer();
storage.referrer = null;
return referrer;
}
function startedUpload(params) {
return sendEvent('sender', 'upload-started', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length + 1,
cm7: storage.totalDownloads,
cd1: params.type,
cd5: takeReferrer()
function cancelledUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'cancel',
time_limit: archive.timeLimit
});
}
function cancelledUpload(params) {
setReferrer('cancelled');
return sendEvent('sender', 'upload-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'cancelled'
function completedUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'ok',
time_limit: archive.timeLimit
});
}
function completedUpload(params) {
return sendEvent('sender', 'upload-stopped', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'completed'
});
}
function addedPassword(params) {
return sendEvent('sender', 'password-added', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads
});
}
function startedDownload(params) {
return sendEvent('recipient', 'download-started', {
cm1: params.size,
cm4: params.ttl,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads
function stoppedUpload(archive) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'error',
time_limit: archive.timeLimit
});
}
function stoppedDownload(params) {
return sendEvent('recipient', 'download-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd2: 'errored',
cd6: params.err
});
}
function cancelledDownload(params) {
setReferrer('cancelled');
return sendEvent('recipient', 'download-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd2: 'cancelled'
});
}
function stoppedUpload(params) {
return sendEvent('sender', 'upload-stopped', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd1: params.type,
cd2: 'errored',
cd6: params.err
});
}
function changedDownloadLimit(params) {
return sendEvent('sender', 'download-limit-changed', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cm8: params.dlimit
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'error'
});
}
function completedDownload(params) {
return sendEvent('recipient', 'download-stopped', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd2: 'completed'
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'ok'
});
}
function deletedUpload(params) {
return sendEvent(category(), 'upload-deleted', {
cm1: params.size,
cm2: params.time,
cm3: params.speed,
cm4: params.ttl,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cd1: params.type,
cd4: params.location
});
}
function unsupported(params) {
return sendEvent(category(), 'unsupported', {
cd6: params.err
});
}
function copiedLink(params) {
return sendEvent('sender', 'copied', {
cd4: params.location
});
}
function exitEvent(target) {
return sendEvent(category(), 'exited', {
cd3: urlToMetric(target.currentTarget.href)
function deletedUpload(ownedFile) {
return addEvent('client_delete', {
age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
downloaded: ownedFile.dtotal > 0,
status: 'ok'
});
}
function experimentEvent(params) {
return sendEvent(category(), 'experiment', params);
return addEvent('client_experiment', params);
}
// eslint-disable-next-line no-unused-vars
function addExitHandlers() {
const links = Array.from(document.querySelectorAll('a'));
links.forEach(l => {
if (/^http/.test(l.getAttribute('href'))) {
l.addEventListener('click', exitEvent);
}
function submittedSignup(params) {
return addEvent('client_login', {
status: 'ok',
trigger: params.trigger
});
}
function restart(state) {
setReferrer(state);
return sendEvent(category(), 'restarted', {
cd2: state
function canceledSignup(params) {
return addEvent('client_login', {
status: 'cancel',
trigger: params.trigger
});
}
function loggedOut(params) {
addEvent('client_logout', {
status: 'ok',
trigger: params.trigger
});
// flush events and start new anon session
submitEvents();
session_id = Date.now();
}
export {
copiedLink,
startedUpload,
cancelledUpload,
stoppedUpload,
completedUpload,
changedDownloadLimit,
deletedUpload,
startedDownload,
cancelledDownload,
stoppedDownload,
completedDownload,
addedPassword,
restart,
unsupported
submittedSignup,
canceledSignup,
loggedOut
};

View file

@ -8,7 +8,6 @@ export default class OwnedFile {
this.url = obj.url;
this.name = obj.name;
this.size = obj.size;
this.type = obj.type;
this.manifest = obj.manifest;
this.time = obj.time;
this.speed = obj.speed;
@ -78,7 +77,6 @@ export default class OwnedFile {
url: this.url,
name: this.name,
size: this.size,
type: this.type,
manifest: this.manifest,
time: this.time,
speed: this.speed,

View file

@ -11,14 +11,7 @@ module.exports = function(app = choo()) {
app.route('/error', body(require('./ui/error')));
app.route('/blank', body(require('./ui/blank')));
app.route('/oauth', async function(state, emit) {
try {
await state.user.finishLogin(state.query.code, state.query.state);
await state.user.syncFileList();
emit('replaceState', '/');
} catch (e) {
emit('replaceState', '/error');
setTimeout(() => emit('render'));
}
emit('authenticate', state.query.code, state.query.state);
});
app.route('*', body(require('./ui/notFound')));
return app;

View file

@ -1,4 +1,4 @@
import { isFile } from './utils';
import { arrayToB64, isFile } from './utils';
import OwnedFile from './ownedFile';
class Mem {
@ -58,6 +58,15 @@ class Storage {
return fs;
}
get id() {
let id = this.engine.getItem('device_id');
if (!id) {
id = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
this.engine.setItem('device_id', id);
}
return id;
}
get totalDownloads() {
return Number(this.engine.getItem('totalDownloads'));
}

View file

@ -1,6 +1,5 @@
const html = require('choo/html');
const Component = require('choo/component');
const signupDialog = require('./signupDialog');
class Account extends Component {
constructor(name, state, emit) {
@ -27,8 +26,7 @@ class Account extends Component {
login(event) {
event.preventDefault();
this.state.modal = signupDialog();
this.emit('render');
this.emit('signup-cta', 'button');
}
logout(event) {

View file

@ -34,7 +34,7 @@ function password(state) {
<input
id="add-password"
type="checkbox"
${state.password ? 'checked' : ''}
${state.archive.password ? 'checked' : ''}
autocomplete="off"
onchange="${togglePasswordInput}"
/>
@ -44,7 +44,7 @@ function password(state) {
</div>
<input
id="password-input"
class="${state.password
class="${state.archive.password
? ''
: 'invisible'} border rounded-sm focus:border-blue leading-normal my-2 py-1 px-2 h-8"
autocomplete="off"
@ -53,7 +53,7 @@ function password(state) {
oninput="${inputChanged}"
onfocus="${focused}"
placeholder="${state.translate('unlockInputPlaceholder')}"
value="${state.password || ''}"
value="${state.archive.password || ''}"
/>
<label
id="password-msg"
@ -74,7 +74,7 @@ function password(state) {
input.classList.add('invisible');
input.value = '';
document.getElementById('password-msg').textContent = '';
state.password = null;
state.archive.password = null;
}
}
@ -91,7 +91,7 @@ function password(state) {
} else {
pwdmsg.textContent = '';
}
state.password = password;
state.archive.password = password;
}
function focused(event) {
@ -219,7 +219,7 @@ module.exports = function(state, emit, archive) {
function del(event) {
event.stopPropagation();
emit('delete', { file: archive, location: 'success-screen' });
emit('delete', archive);
}
function share(event) {
@ -279,11 +279,7 @@ module.exports.wip = function(state, emit) {
event.preventDefault();
event.target.disabled = true;
if (!state.uploading) {
emit('upload', {
type: 'click',
dlimit: state.downloadCount || 1,
password: state.password
});
emit('upload');
}
}
@ -333,9 +329,9 @@ module.exports.uploading = function(state, emit) {
</p>
<div class="text-xs text-grey-dark w-full mt-2 mb-2">
${expiryInfo(state.translate, {
dlimit: state.downloadCount || 1,
dlimit: state.archive.dlimit,
dtotal: 0,
expiresAt: Date.now() + 500 + state.timeLimit * 1000
expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
})}
</div>
<div class="text-blue text-sm font-medium mt-2">${progressPercent}</div>

View file

@ -3,7 +3,6 @@ const html = require('choo/html');
const raw = require('choo/html/raw');
const { secondsToL10nId } = require('../utils');
const selectbox = require('./selectbox');
const signupDialog = require('./signupDialog');
module.exports = function(state, emit) {
const el = html`
@ -29,17 +28,17 @@ module.exports = function(state, emit) {
const dlCountSelect = el.querySelector('#dlCount');
el.replaceChild(
selectbox(
state.downloadCount || 1,
state.archive.dlimit,
counts,
num => state.translate('downloadCount', { num }),
value => {
const max = state.user.maxDownloads;
state.archive.dlimit = Math.min(value, max);
if (value > max) {
state.modal = signupDialog();
value = max;
emit('signup-cta', 'count');
} else {
emit('render');
}
state.downloadCount = value;
emit('render');
},
'expire-after-dl-count-select'
),
@ -53,7 +52,7 @@ module.exports = function(state, emit) {
const timeSelect = el.querySelector('#timespan');
el.replaceChild(
selectbox(
state.timeLimit || 86400,
state.archive.timeLimit,
expires,
num => {
const l10n = secondsToL10nId(num);
@ -61,12 +60,12 @@ module.exports = function(state, emit) {
},
value => {
const max = state.user.maxExpireSeconds;
state.archive.timeLimit = Math.min(value, max);
if (value > max) {
state.modal = signupDialog();
value = max;
emit('signup-cta', 'time');
} else {
emit('render');
}
state.timeLimit = value;
emit('render');
},
'expire-after-time-select'
),

View file

@ -1,8 +1,9 @@
/* global LIMITS */
const html = require('choo/html');
const { bytes, platform } = require('../utils');
const { canceledSignup, submittedSignup } = require('../metrics');
module.exports = function() {
module.exports = function(trigger) {
return function(state, emit, close) {
const hidden = platform() === 'android' ? 'hidden' : '';
let submitting = false;
@ -37,7 +38,7 @@ module.exports = function() {
<button
class="my-4 text-blue hover:text-blue-dark focus:text-blue-darker font-medium"
title="${state.translate('deletePopupCancel')}"
onclick=${close}>${state.translate('deletePopupCancel')}
onclick=${cancel}>${state.translate('deletePopupCancel')}
</button>
</send-signup-dialog>`;
@ -50,6 +51,11 @@ module.exports = function() {
return a.length === 2 && a.every(s => s.length > 0);
}
function cancel(event) {
canceledSignup({ trigger });
close(event);
}
function submitEmail(event) {
event.preventDefault();
if (submitting) {
@ -59,6 +65,7 @@ module.exports = function() {
const el = document.getElementById('email-input');
const email = el.value;
submittedSignup({ trigger });
emit('login', emailish(email) ? email : null);
}
};

View file

@ -9,6 +9,16 @@ import storage from './storage';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const anonId = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
async function hashId(id) {
const d = new Date();
const month = d.getUTCMonth();
const year = d.getUTCFullYear();
const encoded = textEncoder.encode(`${id}:${year}:${month}`);
const hash = await crypto.subtle.digest('SHA-256', encoded);
return arrayToB64(new Uint8Array(hash.slice(16)));
}
export default class User {
constructor(storage) {
@ -25,6 +35,14 @@ export default class User {
this.storage.user = data;
}
get firstAction() {
return this.storage.get('firstAction');
}
set firstAction(action) {
this.storage.set('firstAction', action);
}
get avatar() {
const defaultAvatar = assets.get('user.svg');
if (this.info.avatarDefault) {
@ -63,6 +81,14 @@ export default class User {
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS;
}
async metricId() {
return this.loggedIn ? hashId(this.info.uid) : undefined;
}
async deviceId() {
return this.loggedIn ? hashId(this.storage.id) : hashId(anonId);
}
async login(email) {
const state = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
storage.set('oauthState', state);