Restructure project: group elements and mixing in directories, flatten directory structure

Directly import from core modules, mixins
This commit is contained in:
Martin Kleinschrodt 2018-05-31 17:48:31 +02:00
parent d1f276f46d
commit bffb2648b8
73 changed files with 5098 additions and 45134 deletions

View File

@ -10,11 +10,6 @@
},
"extends": "eslint:recommended",
"plugins": ["html"],
"globals": {
"Polymer": true,
"padlock": true,
"$l": true
},
"rules": {
"brace-style": "off",
"camelcase": "error",
@ -38,7 +33,6 @@
"no-unused-expressions": "off",
"no-use-before-define": "warn",
"no-whitespace-before-property": "error",
"operator-linebreak": ["error", "after"],
"quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": "error",
"semi-spacing": "error",

View File

@ -1,22 +0,0 @@
{
"name": "padlock",
"version": "2.0.0",
"homepage": "http://padlock.io",
"authors": [
"Martin Kleinschrodt <martin@maklesoft.com>"
],
"main": "index.html",
"license": "GPL-3.0",
"ignore": [
"bower_components"
],
"private": true,
"dependencies": {
"polymer": "~2.1.0",
"zxcvbn": "~3.1.0",
"autosize": "~3.0.13",
"iron-list": "https://github.com/MaKleSoft/iron-list.git#custom-fixes",
"paper-spinner": "^2.0.0",
"webcomponentsjs": "1.0.13"
}
}

View File

@ -13,7 +13,7 @@
<link rel="stylesheet" href="./src/styles/fonts.css">
<script type="module" src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script type="module" src="./src/ui/app/app.js"></script>
<script type="module" src="./src/elements/app.js"></script>
</head>
<body>

View File

@ -1,4 +1,4 @@
import { unparse } from "papaparse";
import * as papaparse from "papaparse";
import { Collection, Record } from "./data";
import { MemorySource, EncryptedSource } from "./source";
@ -10,7 +10,9 @@ function recordsToTable(records: Record[]) {
// Two dimensional array, starting with column names
let table = [cols];
// Filter out removed items
records = records.filter(function(rec) { return !rec.removed; });
records = records.filter(function(rec) {
return !rec.removed;
});
// Fill up columns array with distinct field names
for (let rec of records) {
@ -54,7 +56,7 @@ function recordsToTable(records: Record[]) {
}
export function toCSV(records: Record[]): string {
return unparse(recordsToTable(records));
return papaparse.unparse(recordsToTable(records));
}
export async function toPadlock(records: Record[], password: string): Promise<string> {

View File

@ -3,17 +3,13 @@ import { Record, Field } from "./data";
import { Container } from "./crypto";
export class ImportError {
constructor(
public code: "invalid_csv"
) {};
constructor(public code: "invalid_csv") {}
}
//* Detects if a string contains a SecuStore backup
export function isFromSecuStore(data: string): boolean {
return data.indexOf("SecuStore") != -1 &&
data.indexOf("#begin") != -1 &&
data.indexOf("#end") != -1;
};
return data.indexOf("SecuStore") != -1 && data.indexOf("#begin") != -1 && data.indexOf("#end") != -1;
}
export async function fromSecuStore(rawData: string, password: string): Promise<Record[]> {
const begin = "#begin";
@ -45,10 +41,11 @@ export async function fromSecuStore(rawData: string, password: string): Promise<
// Convert the _items_ array of the SecuStore Set object into an array of Padlock records
let records = data.items.map((item: any) => {
let fields = item.template.containsPassword ?
// Passwords are a separate property in SecuStore but will be treated as
// regular fields in Padlock
item.fields.concat([{name: "password", value: item.password}]) : item.fields;
let fields = item.template.containsPassword
? // Passwords are a separate property in SecuStore but will be treated as
// regular fields in Padlock
item.fields.concat([{ name: "password", value: item.password }])
: item.fields;
return new Record(item.title, fields, [data.name]);
});
@ -82,12 +79,11 @@ export function fromTable(data: string[][], nameColIndex?: number, tagsColIndex?
}
}
// All subsequent rows should contain values
let records = data.slice(1).map(function(row) {
// Construct an array of field object from column names and values
let fields = [];
for (let i=0; i<row.length; i++) {
for (let i = 0; i < row.length; i++) {
// Skip name column, category column (if any) and empty fields
if (i != nameColIndex && i != tagsColIndex && row[i]) {
fields.push({
@ -98,7 +94,7 @@ export function fromTable(data: string[][], nameColIndex?: number, tagsColIndex?
}
const tags = row[tagsColIndex!];
return new Record(row[nameColIndex || 0], fields, tags && tags.split(",") || []);
return new Record(row[nameColIndex || 0], fields, (tags && tags.split(",")) || []);
});
return records;
@ -123,7 +119,7 @@ export function isFromPadlock(data: string): boolean {
try {
Container.fromJSON(data);
return true;
} catch(e) {
} catch (e) {
return false;
}
}
@ -171,7 +167,7 @@ function lpParseRow(row: string[]): Record {
{ name: "url", value: row[urlIndex] },
{ name: "username", value: row[usernameIndex] },
{ name: "password", value: row[passwordIndex], masked: true }
]
];
let notes = row[notesIndex];
if (row[urlIndex] === "http://sn") {
@ -191,8 +187,8 @@ function lpParseRow(row: string[]): Record {
}
export function fromLastPass(data: string): Record[] {
let records = parse(data).data
// Remove first row as it only contains field names
let records = parse(data)
.data// Remove first row as it only contains field names
.slice(1)
// Filter out empty rows
.filter(row => row.length > 1)

27
app/src/core/locale.ts Normal file
View File

@ -0,0 +1,27 @@
import { resolveLanguage } from "./util";
import { getLocale } from "./platform";
interface Translations {
[lang: string]: { [msg: string]: string };
}
let translations: Translations = {};
let language: string;
export function localize(msg: string, ...fmtArgs: string[]) {
const lang = translations[language];
let res = (lang && lang[msg]) || msg;
for (let i = 0; i < fmtArgs.length; i++) {
res = res.replace(new RegExp(`\\{${i}\\}`, "g"), fmtArgs[i]);
}
res = res.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
return res;
}
export function loadTranslations(t: Translations) {
translations = t;
language = resolveLanguage(getLocale(), translations);
}

View File

@ -3,21 +3,24 @@ import { Source } from "./source";
import { resolveLanguage } from "./util";
import { getLocale, getPlatformName } from "./platform";
import { Settings } from "./data";
import { satisfies } from "semver";
// import { satisfies } from "semver";
function satisfies(v1: string, v2: string): boolean {
return v1 === v2;
}
export interface Message {
id: string
from: Date
until: Date
link: string
text: string
platform?: string[]
subStatus?: string[]
version?: string
id: string;
from: Date;
until: Date;
link: string;
text: string;
platform?: string[];
subStatus?: string[];
version?: string;
}
export class Messages {
constructor(public url: string, public source: Source, public settings: Settings) {}
private async fetchRead(): Promise<any> {
@ -58,7 +61,8 @@ export class Messages {
})
.filter((a: Message) => {
return (
!read[a.id] && a.from <= now &&
!read[a.id] &&
a.from <= now &&
a.until >= now &&
(!a.platform || a.platform.includes(platform)) &&
(!a.subStatus || a.subStatus.includes(this.settings.syncSubStatus)) &&
@ -68,8 +72,12 @@ export class Messages {
}
async fetch(): Promise<Message[]> {
const req = await request("GET", this.url, undefined,
new Map<string, string>([["Accept", "application/json"]]));
const req = await request(
"GET",
this.url,
undefined,
new Map<string, string>([["Accept", "application/json"]])
);
return this.parseAndFilter(req.responseText);
}
@ -78,6 +86,4 @@ export class Messages {
read[a.id] = true;
await this.saveRead(read);
}
}

View File

@ -1,10 +1,12 @@
import * as moment from "moment";
import "moment-duration-format";
import * as zxcvbn from "zxcvbn";
// RFC4122-compliant uuid generator
export function uuid(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == "x" ? r : (r&0x3|0x8);
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
@ -31,7 +33,7 @@ export function randomString(length = 32, charSet = charSets.full) {
while (str.length < length) {
window.crypto.getRandomValues(rnd);
// Prevent modulo bias by rejecting values larger than the highest muliple of `charSet.length`
if (rnd[0] > 255 - 256 % charSet.length) {
if (rnd[0] > 255 - (256 % charSet.length)) {
continue;
}
str += charSet[rnd[0] % charSet.length];
@ -49,7 +51,7 @@ export function debounce(fn: (...args: any[]) => any, delay: number) {
}
export function wait(dt: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, dt));
return new Promise<void>(resolve => setTimeout(resolve, dt));
}
export function resolveLanguage(locale: string, supportedLanguages: { [lang: string]: any }): string {
@ -75,11 +77,21 @@ export function formatDateFromNow(date: Date) {
return moment(date).fromNow();
}
export function formatDateUntil(startDate: Date|string|number, duration: number) {
const d = moment.duration(moment(startDate).add(duration, "hours").diff(moment()));
export function formatDateUntil(startDate: Date | string | number, duration: number) {
const d = moment.duration(
moment(startDate)
.add(duration, "hours")
.diff(moment())
);
return d.format("hh:mm:ss");
}
export function isFuture(date: Date|string|number, duration: number) {
return moment(date).add(duration, "hours").isAfter();
export function isFuture(date: Date | string | number, duration: number) {
return moment(date)
.add(duration, "hours")
.isAfter();
}
export function passwordStrength(password: string): zxcvbn.ZXCVBNResult {
return zxcvbn(password);
}

View File

@ -1,33 +1,32 @@
import './analytics.js';
import '../../styles/shared.js';
import '../animation/animation.js';
import '../base/base.js';
import '../clipboard/clipboard.js';
import '../cloud-view/cloud-view.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../icon/icon.js';
import '../list-view/list-view.js';
import '../locale/locale.js';
import '../notification/notification.js';
import '../record-view/record-view.js';
import '../settings-view/settings-view.js';
import '../start-view/start-view.js';
import '../sync/sync.js';
import '../title-bar/title-bar.js';
import './auto-lock.js';
import './auto-sync.js';
import './hints.js';
import './messages.js';
import { getPlatformName, getDeviceInfo, isTouch } from "../../core/platform.js"
import "../styles/shared.js";
import "./cloud-view.js";
import "./icon.js";
import "./list-view.js";
import "./record-view.js";
import "./settings-view.js";
import "./start-view.js";
import "./title-bar.js";
import { getPlatformName, getDeviceInfo, isTouch } from "../core/platform.js";
import { applyMixins } from "../core/util.js";
import { BaseElement, html } from "./base.js";
import {
NotificationMixin,
DialogMixin,
MessagesMixin,
DataMixin,
AnimationMixin,
ClipboardMixin,
SyncMixin,
AutoSyncMixin,
AutoLockMixin,
HintsMixin,
AnalyticsMixin,
LocaleMixin
} from "../mixins";
/* global cordova, StatusBar */
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const cordovaReady = new Promise((resolve) => {
const cordovaReady = new Promise(resolve => {
document.addEventListener("deviceready", resolve);
});
@ -46,8 +45,8 @@ class App extends applyMixins(
ClipboardMixin,
LocaleMixin
) {
static get template() {
return Polymer.html`
static get template() {
return html`
<style include="shared">
@keyframes fadeIn {
@ -429,312 +428,318 @@ class App extends applyMixins(
<pl-title-bar></pl-title-bar>
`;
}
}
static get is() { return "pl-app"; }
static get is() {
return "pl-app";
}
static get properties() { return {
locked: {
type: Boolean,
value: true,
observer: "_lockedChanged"
},
_currentView: {
type: String,
value: "",
observer: "_currentViewChanged"
},
_selectedRecord: {
type: Object,
observer: "_selectedRecordChanged"
},
_menuOpen: {
type: Boolean,
value: false,
observer: "_menuOpenChanged"
},
_showingTags: {
type: Boolean,
value: false
}
}; }
static get properties() {
return {
locked: {
type: Boolean,
value: true,
observer: "_lockedChanged"
},
_currentView: {
type: String,
value: "",
observer: "_currentViewChanged"
},
_selectedRecord: {
type: Object,
observer: "_selectedRecordChanged"
},
_menuOpen: {
type: Boolean,
value: false,
observer: "_menuOpenChanged"
},
_showingTags: {
type: Boolean,
value: false
}
};
}
constructor() {
super();
constructor() {
super();
// If we want to capture all keydown events, we have to add the listener
// directly to the document
document.addEventListener("keydown", this._keydown.bind(this), false);
// If we want to capture all keydown events, we have to add the listener
// directly to the document
document.addEventListener("keydown", this._keydown.bind(this), false);
// Listen for android back button
document.addEventListener("backbutton", this._back.bind(this), false);
// Listen for android back button
document.addEventListener("backbutton", this._back.bind(this), false);
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
}
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
}
get _isNarrow() {
return this.offsetWidth < 600;
}
get _isNarrow() {
return this.offsetWidth < 600;
}
connectedCallback() {
super.connectedCallback();
let isIPhoneX;
getDeviceInfo()
.then((device) => {
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
if (isIPhoneX) {
Object.assign(document.body.style, {
margin: 0,
height: "812px",
position: "relative"
});
}
return cordovaReady;
})
.then(() => {
// Replace window.open method with the inappbrowser equivalent
window.open = cordova.InAppBrowser.open;
if (isIPhoneX) {
StatusBar && StatusBar.show();
}
navigator.splashscreen.hide();
});
connectedCallback() {
super.connectedCallback();
let isIPhoneX;
getDeviceInfo()
.then(device => {
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
if (isIPhoneX) {
Object.assign(document.body.style, {
margin: 0,
height: "812px",
position: "relative"
});
}
return cordovaReady;
})
.then(() => {
// Replace window.open method with the inappbrowser equivalent
window.open = cordova.InAppBrowser.open;
if (isIPhoneX) {
StatusBar && StatusBar.show();
}
navigator.splashscreen.hide();
});
getPlatformName().then((platform) => {
const className = platform.toLowerCase().replace(/ /g, "-");
if (className) {
this.classList.add(className);
this.root.querySelector("pl-title-bar").classList.add(className);
}
});
getPlatformName().then(platform => {
const className = platform.toLowerCase().replace(/ /g, "-");
if (className) {
this.classList.add(className);
this.root.querySelector("pl-title-bar").classList.add(className);
}
});
if (!isTouch()) {
window.addEventListener("focus", () => setTimeout(() => {
if (this.locked) {
this.$.startView.focus();
}
}, 100));
}
}
if (!isTouch()) {
window.addEventListener("focus", () =>
setTimeout(() => {
if (this.locked) {
this.$.startView.focus();
}
}, 100)
);
}
}
recordDeleted(record) {
if (record === this._selectedRecord) {
this.$.listView.deselect();
}
}
recordDeleted(record) {
if (record === this._selectedRecord) {
this.$.listView.deselect();
}
}
dataLoaded() {
this.locked = false;
this.$.startView.open = true;
}
dataLoaded() {
this.locked = false;
this.$.startView.open = true;
}
dataUnloaded() {
this.clearDialogs();
this.$.startView.reset();
this.locked = true;
this.$.startView.open = false;
this.clearClipboard();
}
dataUnloaded() {
this.clearDialogs();
this.$.startView.reset();
this.locked = true;
this.$.startView.open = false;
this.clearClipboard();
}
dataReset() {
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
}
dataReset() {
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
}
_closeRecord() {
this.$.listView.deselect();
}
_closeRecord() {
this.$.listView.deselect();
}
_selectedRecordChanged() {
clearTimeout(this._selectedRecordChangedTimeout);
this._selectedRecordChangedTimeout = setTimeout(() => {
if (this._selectedRecord) {
this.$.recordView.record = this._selectedRecord;
this._currentView = "recordView";
this._selectedRecord.lastUsed = new Date();
this.saveCollection();
} else if (this._currentView == "recordView") {
this._currentView = "";
}
}, 10);
}
_selectedRecordChanged() {
clearTimeout(this._selectedRecordChangedTimeout);
this._selectedRecordChangedTimeout = setTimeout(() => {
if (this._selectedRecord) {
this.$.recordView.record = this._selectedRecord;
this._currentView = "recordView";
this._selectedRecord.lastUsed = new Date();
this.saveCollection();
} else if (this._currentView == "recordView") {
this._currentView = "";
}
}, 10);
}
_openSettings() {
this._currentView = "settingsView";
this.$.listView.deselect();
}
_openSettings() {
this._currentView = "settingsView";
this.$.listView.deselect();
}
_settingsBack() {
this._currentView = "";
}
_settingsBack() {
this._currentView = "";
}
_openCloudView() {
this._currentView = "cloudView";
this.refreshAccount();
this.$.listView.deselect();
if (!this.settings.syncConnected && !isTouch()) {
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
}
}
_openCloudView() {
this._currentView = "cloudView";
this.refreshAccount();
this.$.listView.deselect();
if (!this.settings.syncConnected && !isTouch()) {
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
}
}
_cloudViewBack() {
this._currentView = "";
}
_cloudViewBack() {
this._currentView = "";
}
_currentViewChanged(curr, prev) {
this.$.main.classList.toggle("showing-pages", !!curr);
_currentViewChanged(curr, prev) {
this.$.main.classList.toggle("showing-pages", !!curr);
const currView = this.$[curr];
const prevView = this.$[prev];
if (currView) {
this.animateElement(currView, {
animation: "viewIn",
duration: 400,
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
fill: "backwards"
});
currView.classList.add("showing");
currView.animate();
}
if (prevView) {
this.animateElement(prevView, {
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
duration: 400,
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
fill: "forwards"
});
setTimeout(() => prevView.classList.remove("showing"), 350);
}
}
const currView = this.$[curr];
const prevView = this.$[prev];
if (currView) {
this.animateElement(currView, {
animation: "viewIn",
duration: 400,
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
fill: "backwards"
});
currView.classList.add("showing");
currView.animate();
}
if (prevView) {
this.animateElement(prevView, {
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
duration: 400,
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
fill: "forwards"
});
setTimeout(() => prevView.classList.remove("showing"), 350);
}
}
//* Keyboard shortcuts
_keydown(event) {
if (this.locked || padlock.Input.activeInput) {
return;
}
//* Keyboard shortcuts
_keydown(event) {
if (this.locked || padlock.Input.activeInput) {
return;
}
let shortcut;
const control = event.ctrlKey || event.metaKey;
let shortcut;
const control = event.ctrlKey || event.metaKey;
// ESCAPE -> Back
if (event.key === "Escape") {
shortcut = () => this._back();
}
// CTRL/CMD + F -> Filter
else if (control && event.key === "f") {
shortcut = () => this.$.listView.search();
}
// CTRL/CMD + N -> New Record
else if (control && event.key === "n") {
shortcut = () => this.createRecord();
}
// ESCAPE -> Back
if (event.key === "Escape") {
shortcut = () => this._back();
}
// CTRL/CMD + F -> Filter
else if (control && event.key === "f") {
shortcut = () => this.$.listView.search();
}
// CTRL/CMD + N -> New Record
else if (control && event.key === "n") {
shortcut = () => this.createRecord();
}
// If one of the shortcuts matches, execute it and prevent the default behaviour
if (shortcut) {
shortcut();
event.preventDefault();
} else if (event.key.length === 1) {
this.$.listView.search();
}
}
// If one of the shortcuts matches, execute it and prevent the default behaviour
if (shortcut) {
shortcut();
event.preventDefault();
} else if (event.key.length === 1) {
this.$.listView.search();
}
}
_back() {
switch (this._currentView) {
case "recordView":
this._closeRecord();
break;
case "settingsView":
this._settingsBack();
break;
case "cloudView":
this._cloudViewBack();
break;
default:
if (this.$.listView.filterActive) {
this.$.listView.clearFilter();
} else {
navigator.Backbutton && navigator.Backbutton.goBack();
}
}
}
_back() {
switch (this._currentView) {
case "recordView":
this._closeRecord();
break;
case "settingsView":
this._settingsBack();
break;
case "cloudView":
this._cloudViewBack();
break;
default:
if (this.$.listView.filterActive) {
this.$.listView.clearFilter();
} else {
navigator.Backbutton && navigator.Backbutton.goBack();
}
}
}
_lockedChanged() {
if (this.locked) {
this._currentView = "";
this.$.main.classList.remove("active");
this._menuOpen = false;
} else {
setTimeout(() => {
this.$.main.classList.add("active");
}, 600);
}
}
_lockedChanged() {
if (this.locked) {
this._currentView = "";
this.$.main.classList.remove("active");
this._menuOpen = false;
} else {
setTimeout(() => {
this.$.main.classList.add("active");
}, 600);
}
}
_menuOpenChanged() {
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
this.$.main.classList.toggle("show-menu", this._menuOpen);
if (!this._menuOpen) {
setTimeout(() => this._showingTags = false, 300);
}
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
duration: 400,
fullDuration: 600,
initialDelay: 50,
fill: "both"
});
}
_menuOpenChanged() {
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
this.$.main.classList.toggle("show-menu", this._menuOpen);
if (!this._menuOpen) {
setTimeout(() => (this._showingTags = false), 300);
}
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
duration: 400,
fullDuration: 600,
initialDelay: 50,
fill: "both"
});
}
_toggleMenu() {
this._menuOpen = !this._menuOpen;
}
_toggleMenu() {
this._menuOpen = !this._menuOpen;
}
_lock() {
if (this.isSynching) {
this.alert($l("Cannot lock app while sync is in progress!"));
} else {
this.unloadData();
}
}
_lock() {
if (this.isSynching) {
this.alert($l("Cannot lock app while sync is in progress!"));
} else {
this.unloadData();
}
}
_newRecord() {
this.createRecord();
}
_newRecord() {
this.createRecord();
}
_enableMultiSelect() {
this.$.listView.multiSelect = true;
}
_enableMultiSelect() {
this.$.listView.multiSelect = true;
}
_menuWrapperClicked() {
setTimeout(() => this._menuOpen = false, 50);
}
_menuWrapperClicked() {
setTimeout(() => (this._menuOpen = false), 50);
}
_showTags(e) {
this._menuOpen = true;
this._showingTags = true;
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
animation: "tagIn",
duration: 400,
fullDuration: 600,
fill: "both"
});
e.stopPropagation();
}
_showTags(e) {
this._menuOpen = true;
this._showingTags = true;
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
animation: "tagIn",
duration: 400,
fullDuration: 600,
fill: "both"
});
e.stopPropagation();
}
_closeTags(e) {
this._showingTags = false;
e.stopPropagation();
}
_closeTags(e) {
this._showingTags = false;
e.stopPropagation();
}
_selectTag(e) {
setTimeout(() => {
this.$.listView.filterString = e.model.item;
}, 350);
}
_selectTag(e) {
setTimeout(() => {
this.$.listView.filterString = e.model.item;
}, 350);
}
_hasTags() {
return !!this.collection.tags.length;
}
_hasTags() {
return !!this.collection.tags.length;
}
}
window.customElements.define(App.is, App);

View File

@ -1,11 +1,7 @@
import "@polymer/polymer/polymer-legacy";
import { PolymerElement, html } from "@polymer/polymer/polymer-element";
import "../../padlock.js";
window.Polymer = { html };
import "@polymer/polymer/polymer-legacy.js";
import { PolymerElement, html } from "@polymer/polymer/polymer-element.js";
export class BaseElement extends PolymerElement {
truthy(val) {
return !!val;
}
@ -17,7 +13,6 @@ export class BaseElement extends PolymerElement {
identity(val) {
return val;
}
}
padlock.BaseElement = BaseElement;
export { html };

View File

@ -0,0 +1,112 @@
import "../styles/shared.js";
import { setClipboard } from "../core/platform.js";
import { BaseElement, html } from "./base.js";
import { LocaleMixin } from "../mixins";
class Clipboard extends LocaleMixin(BaseElement) {
static get template() {
return html`
<style include="shared">
:host {
display: flex;
text-align: center;
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
position: fixed;
left: 15px;
right: 15px;
bottom: 15px;
z-index: 10;
max-width: 400px;
margin: 0 auto;
border-radius: var(--border-radius);
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
color: var(--color-background);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
}
:host(:not(.showing)) {
transform: translateY(130%);
}
.content {
flex: 1;
padding: 15px;
}
.name {
font-weight: bold;
}
button {
height: auto;
line-height: normal;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.countdown {
font-size: var(--font-size-small);
}
</style>
<div class="content">
<div class="title">[[ \$l("Copied To Clipboard:") ]]</div>
<div class="name">[[ record.name ]] / [[ field.name ]]</div>
</div>
<button class="tiles-2 tap" on-click="clear">
<div><strong>[[ \$l("Clear") ]]</strong></div>
<div class="countdown">[[ _tMinusClear ]]s</div>
</button>
`;
}
static get is() {
return "pl-clipboard";
}
static get properties() {
return {
record: Object,
field: Object
};
}
set(record, field, duration = 60) {
clearInterval(this._interval);
this.record = record;
this.field = field;
setClipboard(field.value);
this.classList.add("showing");
const tStart = Date.now();
this._tMinusClear = duration;
this._interval = setInterval(() => {
const dt = tStart + duration * 1000 - Date.now();
if (dt <= 0) {
this.clear();
} else {
this._tMinusClear = Math.floor(dt / 1000);
}
}, 1000);
return new Promise(resolve => {
this._resolve = resolve;
});
}
clear() {
clearInterval(this._interval);
setClipboard(" ");
this.classList.remove("showing");
typeof this._resolve === "function" && this._resolve();
this._resolve = null;
}
}
window.customElements.define(Clipboard.is, Clipboard);

View File

@ -1,19 +1,15 @@
import '../../styles/shared.js';
import '../animation/animation.js';
import '../base/base.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../icon/icon.js';
import '../input/input.js';
import '../loading-button/loading-button.js';
import '../locale/locale.js';
import '../notification/notification.js';
import '../promo/promo.js';
import '../sync/sync.js';
import '../toggle/toggle-button.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import "./icon.js";
import "./input.js";
import "./loading-button.js";
import "./promo.js";
import "./toggle-button.js";
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
import { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin } from "../mixins";
import { applyMixins } from "../core/util.js";
import { localize as $l } from "../core/locale.js";
class CloudView extends applyMixins(
BaseElement,
@ -24,8 +20,8 @@ class CloudView extends applyMixins(
NotificationMixin,
AnimationMixin
) {
static get template() {
return Polymer.html`
static get template() {
return html`
<style include="shared">
:host {
display: flex;
@ -310,135 +306,129 @@ class CloudView extends applyMixins(
<div class="rounded-corners"></div>
`;
}
}
static get is() { return "pl-cloud-view"; }
static get is() {
return "pl-cloud-view";
}
static get properties() {
static get properties() {}
}
ready() {
super.ready();
this.listen("data-loaded", () => this.animate());
this.listen("sync-connect-start", () => this.animate());
this.listen("sync-connect-cancel", () => this.animate());
this.listen("sync-connect-success", () => this.animate());
this.listen("sync-disconnect", () => this.animate());
this.listen("sync-connect-success", () => this.refreshAccount());
}
ready() {
super.ready();
this.listen("data-loaded", () => this.animate());
this.listen("sync-connect-start", () => this.animate());
this.listen("sync-connect-cancel", () => this.animate());
this.listen("sync-connect-success", () => this.animate());
this.listen("sync-disconnect", () => this.animate());
this.listen("sync-connect-success", () => this.refreshAccount());
}
animate() {
if (this.settings.syncConnected) {
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
}
}
animate() {
if (this.settings.syncConnected) {
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
}
}
focusEmailInput() {
this.$.emailInput.focus();
}
focusEmailInput() {
this.$.emailInput.focus();
}
_credentialsChanged() {
if (this.isActivationPending() && !this._testCredsTimeout) {
// Wait 1 minute, then poll every 10 seconds
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
// Also test on first focus event since there is a chance the user is just returning
// from his email client / web browsers
window.addEventListener("focus", () => this.testCredentials(), { once: true });
} else if (!this.isActivationPending()) {
clearTimeout(this._testCredsTimeout);
this._testCredsTimeout = null;
}
}
_credentialsChanged() {
if (this.isActivationPending() && !this._testCredsTimeout) {
// Wait 1 minute, then poll every 10 seconds
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
// Also test on first focus event since there is a chance the user is just returning
// from his email client / web browsers
window.addEventListener("focus", () => this.testCredentials(), { once: true });
} else if (!this.isActivationPending()) {
clearTimeout(this._testCredsTimeout);
this._testCredsTimeout = null;
}
}
_back() {
this.dispatchEvent(new CustomEvent("cloud-back"));
}
_back() {
this.dispatchEvent(new CustomEvent("cloud-back"));
}
_logout() {
this.confirm($l("Are you sure you want to log out?"), $l("Log Out")).then(confirmed => {
if (confirmed) {
this.disconnectCloud();
}
});
}
_logout() {
this.confirm(
$l("Are you sure you want to log out?"),
$l("Log Out")
).then((confirmed) => {
if (confirmed) {
this.disconnectCloud();
}
});
}
_login() {
if (this._submittingEmail) {
return;
}
_login() {
if (this._submittingEmail) {
return;
}
this.$.loginButton.start();
this.$.loginButton.start();
if (this.$.emailInput.invalid) {
this.alert($l("Please enter a valid email address!")).then(() => this.$.emailInput.focus());
this.$.loginButton.fail();
return;
}
if (this.$.emailInput.invalid) {
this.alert($l("Please enter a valid email address!"))
.then(() => this.$.emailInput.focus());
this.$.loginButton.fail();
return;
}
this._submittingEmail = true;
this._submittingEmail = true;
this.connectCloud(this.$.emailInput.value)
.then(() => {
this._submittingEmail = false;
this.$.loginButton.success();
return this.promptLoginCode();
})
.then(() => {
if (this.settings.syncConnected) {
this.synchronize();
}
})
.catch(() => {
this._submittingEmail = false;
this.$.loginButton.fail();
});
}
this.connectCloud(this.$.emailInput.value)
.then(() => {
this._submittingEmail = false;
this.$.loginButton.success();
return this.promptLoginCode();
})
.then(() => {
if (this.settings.syncConnected) {
this.synchronize();
}
})
.catch(() => {
this._submittingEmail = false;
this.$.loginButton.fail();
});
_isCurrentDevice(device) {
return this.settings.syncId === device.tokenId;
}
}
_revokeDevice(e) {
const device = e.model.item;
this.confirm($l('Do you want to revoke access to for the device "{0}"?', device.description)).then(
confirmed => {
if (confirmed) {
this.cloudSource.source.revokeAuthToken(device.tokenId).then(() => {
this.refreshAccount();
this.alert($l("Access for {0} revoked successfully!", device.description), { type: "success" });
});
}
}
);
}
_isCurrentDevice(device) {
return this.settings.syncId === device.tokenId;
}
_contactSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_revokeDevice(e) {
const device = e.model.item;
this.confirm($l("Do you want to revoke access to for the device \"{0}\"?", device.description))
.then((confirmed) => {
if (confirmed) {
this.cloudSource.source.revokeAuthToken(device.tokenId)
.then(() => {
this.refreshAccount();
this.alert($l("Access for {0} revoked successfully!", device.description),
{ type: "success" });
});
}
});
}
_paymentSourceLabel() {
const s = this.account && this.account.paymentSource;
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
}
_contactSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_buySubscription(e) {
this.buySubscription(e.target.dataset.source);
}
_paymentSourceLabel() {
const s = this.account && this.account.paymentSource;
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
}
_updatePaymentMethod(e) {
this.updatePaymentMethod(e.target.dataset.source);
}
_buySubscription(e) {
this.buySubscription(e.target.dataset.source);
}
_updatePaymentMethod(e) {
this.updatePaymentMethod(e.target.dataset.source);
}
_promoExpired() {
this.dispatch("settings-changed");
}
_promoExpired() {
this.dispatch("settings-changed");
}
}
window.customElements.define(CloudView.is, CloudView);

View File

@ -0,0 +1,128 @@
import "../styles/shared.js";
import { localize } from "../core/locale.js";
import { BaseElement, html } from "./base.js";
import "./dialog.js";
const defaultButtonLabel = localize("OK");
class DialogAlert extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
};
}
:host([type="warning"]) {
--pl-dialog-inner: {
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
};
}
:host([type="plain"]) {
--pl-dialog-inner: {
background: var(--color-background);
};
}
:host([hide-icon]) .info-icon {
display: none;
}
:host([hide-icon]) .info-text,
:host([hide-icon]) .info-title {
text-align: center;
}
.info-text:not(.small) {
font-size: var(--font-size-default);
}
</style>
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
<div class="info" hidden\$="[[ _hideInfo(title, message) ]]">
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
<div class="info-body">
<div class="info-title">[[ title ]]</div>
<div class\$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
</div>
</div>
<template is="dom-repeat" items="[[ options ]]">
<button on-click="_selectOption" class\$="[[ _buttonClass(index) ]]">[[ item ]]</button>
</template>
</pl-dialog>
`;
}
static get is() {
return "pl-dialog-alert";
}
static get properties() {
return {
buttonLabel: { type: String, value: defaultButtonLabel },
title: { type: String, value: "" },
message: { type: String, value: "" },
options: { type: Array, value: ["OK"] },
preventDismiss: { type: Boolean, value: false },
type: { type: String, value: "info", reflectToAttribute: true },
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
open: { type: Boolean, value: false }
};
}
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
this.message = message;
this.title = title;
this.type = type;
this.preventDismiss = preventDismiss;
this.options = options;
this.hideIcon = hideIcon;
setTimeout(() => (this.open = true), 10);
return new Promise(resolve => {
this._resolve = resolve;
});
}
_icon() {
switch (this.type) {
case "info":
return "info-round";
case "warning":
return "error";
case "success":
return "success";
case "question":
return "question";
}
}
_selectOption(e) {
this.open = false;
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
this._resolve = null;
}
_dialogDismiss() {
typeof this._resolve === "function" && this._resolve();
this._resolve = null;
}
_textClass() {
return this.title ? "small" : "";
}
_buttonClass(index) {
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
}
_hideInfo() {
return !this.title && !this.message;
}
}
window.customElements.define(DialogAlert.is, DialogAlert);

View File

@ -0,0 +1,62 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { localize } from "../core/locale.js";
import "./dialog.js";
const defaultMessage = localize("Are you sure you want to do this?");
const defaultConfirmLabel = localize("Confirm");
const defaultCancelLabel = localize("Cancel");
class DialogConfirm extends BaseElement {
static get template() {
return html`
<style include="shared"></style>
<pl-dialog open="{{ open }}" prevent-dismiss="">
<div class="message tiles-1">{{ message }}</div>
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
</pl-dialog>
`;
}
static get is() {
return "pl-dialog-confirm";
}
static get properties() {
return {
confirmLabel: { type: String, value: defaultConfirmLabel },
cancelLabel: { type: String, value: defaultCancelLabel },
message: { type: String, value: defaultMessage },
open: { type: Boolean, value: false }
};
}
_confirm() {
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
this.open = false;
typeof (this._resolve === "function") && this._resolve(true);
this._resolve = null;
}
_cancel() {
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
this.open = false;
typeof (this.resolve === "function") && this._resolve(false);
this._resolve = null;
}
confirm(message, confirmLabel, cancelLabel) {
this.message = message || defaultMessage;
this.confirmLabel = confirmLabel || defaultConfirmLabel;
this.cancelLabel = cancelLabel || defaultCancelLabel;
this.open = true;
return new Promise(resolve => {
this._resolve = resolve;
});
}
}
window.customElements.define(DialogConfirm.is, DialogConfirm);

View File

@ -1,13 +1,12 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import './export.js';
const { BaseElement, LocaleMixin } = padlock;
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import "./dialog.js";
import "./export.js";
import { LocaleMixin } from "../mixins";
class PlExportDialog extends LocaleMixin(BaseElement) {
static get template() {
return Polymer.html`
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
@ -25,22 +24,26 @@ class PlExportDialog extends LocaleMixin(BaseElement) {
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
</pl-dialog>
`;
}
}
static get is() { return "pl-export-dialog"; }
static get is() {
return "pl-dialog-export";
}
static get properties() { return {
records: Array
}; }
static get properties() {
return {
records: Array
};
}
_close() {
this.$.dialog.open = false;
}
_close() {
this.$.dialog.open = false;
}
export(records) {
this.records = records;
this.$.dialog.open = true;
}
export(records) {
this.records = records;
this.$.dialog.open = true;
}
}
window.customElements.define(PlExportDialog.is, PlExportDialog);

View File

@ -1,14 +1,12 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import '../icon/icon.js';
import '../locale/locale.js';
const { BaseElement, LocaleMixin } = padlock;
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import "./dialog.js";
import "./icon.js";
import { LocaleMixin } from "../mixins";
class RecordFieldDialog extends LocaleMixin(BaseElement) {
static get template() {
return Polymer.html`
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
@ -123,98 +121,105 @@ class RecordFieldDialog extends LocaleMixin(BaseElement) {
</div>
</pl-dialog>
`;
}
}
static get is() { return "pl-record-field-dialog"; }
static get is() {
return "pl-dialog-field";
}
static get properties() { return {
editing: {
type: Boolean,
value: false,
reflectToAttribute: true
},
field: {
type: Object,
value: () => { return { name: "", value: "" }; }
},
open: {
type: Boolean,
value: false
}
}; }
static get properties() {
return {
editing: {
type: Boolean,
value: false,
reflectToAttribute: true
},
field: {
type: Object,
value: () => {
return { name: "", value: "" };
}
},
open: {
type: Boolean,
value: false
}
};
}
openField(field, edit = false, presets = {}) {
this.open = true;
this.editing = false;
this.field = field;
this.$.nameInput.value = presets.name || this.field.name;
this.$.valueInput.value = presets.value || this.field.value;
if (edit) {
this._edit();
}
return new Promise((resolve) => {
this._resolve = resolve;
});
}
openField(field, edit = false, presets = {}) {
this.open = true;
this.editing = false;
this.field = field;
this.$.nameInput.value = presets.name || this.field.name;
this.$.valueInput.value = presets.value || this.field.value;
if (edit) {
this._edit();
}
return new Promise(resolve => {
this._resolve = resolve;
});
}
_closeWithAction(action) {
this.open = false;
this._resolve && this._resolve({
action: action,
name: this.$.nameInput.value,
value: this.$.valueInput.value
});
this._resolve = null;
}
_closeWithAction(action) {
this.open = false;
this._resolve &&
this._resolve({
action: action,
name: this.$.nameInput.value,
value: this.$.valueInput.value
});
this._resolve = null;
}
_close() {
this._closeWithAction();
}
_close() {
this._closeWithAction();
}
_delete() {
this._closeWithAction("delete");
}
_delete() {
this._closeWithAction("delete");
}
_copy() {
this._closeWithAction("copy");
}
_copy() {
this._closeWithAction("copy");
}
_generate() {
this._closeWithAction("generate");
}
_generate() {
this._closeWithAction("generate");
}
_edit() {
this.editing = true;
setTimeout(() => {
if (!this.$.nameInput.value) {
this.$.nameInput.focus();
} else {
this.$.valueInput.focus();
}
}, 100);
}
_edit() {
this.editing = true;
setTimeout(() => {
if (!this.$.nameInput.value) {
this.$.nameInput.focus();
} else {
this.$.valueInput.focus();
}
}, 100);
}
_discardChanges() {
this.$.nameInput.value = this.field.name;
this.$.valueInput.value = this.field.value;
this._close();
}
_discardChanges() {
this.$.nameInput.value = this.field.name;
this.$.valueInput.value = this.field.value;
this._close();
}
_saveChanges() {
this.field.name = this.$.nameInput.value;
this.field.value = this.$.valueInput.value;
this._closeWithAction("edited");
}
_saveChanges() {
this.field.name = this.$.nameInput.value;
this.field.value = this.$.valueInput.value;
this._closeWithAction("edited");
}
_inputClicked(e) {
if (!e.target.value) {
this.editing = true;
}
}
_inputClicked(e) {
if (!e.target.value) {
this.editing = true;
}
}
_nameInputEnter() {
this.$.valueInput.focus();
}
_nameInputEnter() {
this.$.valueInput.focus();
}
}
window.customElements.define(RecordFieldDialog.is, RecordFieldDialog);

View File

@ -0,0 +1,66 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { localize } from "../core/locale.js";
import "./dialog.js";
class DialogOptions extends BaseElement {
static get template() {
return html`
<style include="shared"></style>
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp="">
<div class="message tiles-1">[[ message ]]</div>
</template>
<template is="dom-repeat" items="[[ options ]]">
<button class\$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
</template>
</pl-dialog>
`;
}
static get is() {
return "pl-dialog-options";
}
static get properties() {
return {
message: { type: String, value: "" },
open: { type: Boolean, value: false },
options: { type: Array, value: [localize("Dismiss")] },
preventDismiss: { type: Boolean, value: false }
};
}
choose(message, options) {
this.message = message || "";
this.options = options || this.options;
setTimeout(() => (this.open = true), 50);
return new Promise(resolve => {
this._resolve = resolve;
});
}
_selectOption(e) {
this.open = false;
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
this._resolve = null;
}
_buttonClass(index) {
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
}
_hasMessage(message) {
return !!message;
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(-1);
this._resolve = null;
}
}
window.customElements.define(DialogOptions.is, DialogOptions);

View File

@ -1,18 +1,19 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import '../icon/icon.js';
import '../input/input.js';
import '../loading-button/loading-button.js';
import '../locale/locale.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { applyMixins, formatDateUntil } from "../core/util.js";
import { track } from "../core/tracking.js";
import { localize as $l } from "../core/locale.js";
import "./dialog.js";
import "./icon.js";
import "./input.js";
import "./loading-button.js";
import { LocaleMixin } from "../mixins";
const { LocaleMixin, BaseElement } = padlock;
const { applyMixins, formatDateUntil } = padlock.util;
const { track } = padlock.tracking;
/* global Stripe */
let stripe;
const stripeLoaded = new Promise((resolve) => {
const stripeLoaded = new Promise(resolve => {
const script = document.createElement("script");
script.src = "https://js.stripe.com/v3/";
script.async = true;
@ -20,12 +21,9 @@ const stripeLoaded = new Promise((resolve) => {
document.body.appendChild(script);
});
class PaymentDialog extends applyMixins(
BaseElement,
LocaleMixin
) {
static get template() {
return Polymer.html`
class PaymentDialog extends applyMixins(BaseElement, LocaleMixin) {
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-max-width: 500px;
@ -307,180 +305,182 @@ class PaymentDialog extends applyMixins(
</button>
</pl-dialog>
`;
}
}
static get is() { return "pl-payment-dialog"; }
static get is() {
return "pl-dialog-payment";
}
static get properties() { return {
open: { type: Boolean },
promo: { type: Object, value: null },
plan: { type: Object, value: null },
source: Object,
stripePubKey: { type: String, value: "" },
csrfToken: { stype: String, value: ""},
remainingTrialDays: { type: Number, value: 0 },
_cardError: { type: String, value: "" },
_needsSupport: { type: Boolean, value: false },
_price: {
type: Array,
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
},
_originalPrice: {
type: Array,
computed: "_calcPrice(plan.amount)"
}
}; }
static get properties() {
return {
open: { type: Boolean },
promo: { type: Object, value: null },
plan: { type: Object, value: null },
source: Object,
stripePubKey: { type: String, value: "" },
csrfToken: { stype: String, value: "" },
remainingTrialDays: { type: Number, value: 0 },
_cardError: { type: String, value: "" },
_needsSupport: { type: Boolean, value: false },
_price: {
type: Array,
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
},
_originalPrice: {
type: Array,
computed: "_calcPrice(plan.amount)"
}
};
}
static get observers() { return [
"_setupCountdown(promo.redeemWithin)"
]; }
static get observers() {
return ["_setupCountdown(promo.redeemWithin)"];
}
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
}, 1000);
}
}
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
}, 1000);
}
}
show(source) {
this._needsSupport = false;
this._cardError = "";
this.open = true;
this._source = source;
show(source) {
this._needsSupport = false;
this._cardError = "";
this.open = true;
this._source = source;
if (!this._cardElement) {
this._setupPayment();
}
if (!this._cardElement) {
this._setupPayment();
}
track("Payment Dialog: Open", {
"Plan": this.plan && this.plan.id,
"Source": this._source
});
track("Payment Dialog: Open", {
Plan: this.plan && this.plan.id,
Source: this._source
});
return new Promise((resolve) => {
this._resolve = resolve;
});
}
return new Promise(resolve => {
this._resolve = resolve;
});
}
_setupPayment() {
stripeLoaded.then(() => {
stripe = Stripe(this.stripePubKey);
const elements = stripe.elements();
const card = this._cardElement = elements.create("card", {
iconStyle: "solid",
style: {
base: {
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
fontSize: "18px"
},
invalid: {
textShadow: "none"
}
}
});
_setupPayment() {
stripeLoaded.then(() => {
stripe = Stripe(this.stripePubKey);
const elements = stripe.elements();
const card = (this._cardElement = elements.create("card", {
iconStyle: "solid",
style: {
base: {
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
fontSize: "18px"
},
invalid: {
textShadow: "none"
}
}
}));
const cardElement = document.createElement("div");
this.appendChild(cardElement);
card.mount(cardElement);
const cardElement = document.createElement("div");
this.appendChild(cardElement);
card.mount(cardElement);
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
});
}
card.addEventListener("change", e => (this._cardError = (e.error && e.error.message) || ""));
});
}
_submitCard() {
if (this._submittingCard) {
return;
}
_submitCard() {
if (this._submittingCard) {
return;
}
this.$.submitButton.start();
this._submittingCard = true;
this.$.submitButton.start();
this._submittingCard = true;
const coupon = this.promo && this.promo.coupon.id || "";
const coupon = (this.promo && this.promo.coupon.id) || "";
stripe.createToken(this._cardElement).then((result) => {
const edata = {
"Plan": this.plan && this.plan.id,
"Source": this._source,
"Success": true,
"Token Created": true,
"Coupon": coupon
};
stripe.createToken(this._cardElement).then(result => {
const edata = {
Plan: this.plan && this.plan.id,
Source: this._source,
Success: true,
"Token Created": true,
Coupon: coupon
};
if (result.error) {
this.$.submitButton.fail();
this._submittingCard = false;
Object.assign(edata, {
"Error Code": result.error.code,
"Error Type": result.error.type,
"Error Message": result.error.message,
"Success": false,
"Token Created": false
});
track("Payment Dialog: Submit", edata);
} else {
this.source.subscribe(result.token.id, coupon, this._source)
.then(() => {
this.$.submitButton.success();
typeof this._resolve === "function" && this._resolve(true);
this._submittingCard = false;
this._resolve = null;
this.open = false;
track("Payment Dialog: Submit", edata);
})
.catch((e) => {
this.$.submitButton.fail();
this._submittingCard = false;
this._cardError = e.message;
this._needsSupport = true;
Object.assign(edata, {
"Error Code": e.code,
"Error Message": e.message,
"Success": false
});
track("Payment Dialog: Submit", edata);
});
}
});
}
if (result.error) {
this.$.submitButton.fail();
this._submittingCard = false;
Object.assign(edata, {
"Error Code": result.error.code,
"Error Type": result.error.type,
"Error Message": result.error.message,
Success: false,
"Token Created": false
});
track("Payment Dialog: Submit", edata);
} else {
this.source
.subscribe(result.token.id, coupon, this._source)
.then(() => {
this.$.submitButton.success();
typeof this._resolve === "function" && this._resolve(true);
this._submittingCard = false;
this._resolve = null;
this.open = false;
track("Payment Dialog: Submit", edata);
})
.catch(e => {
this.$.submitButton.fail();
this._submittingCard = false;
this._cardError = e.message;
this._needsSupport = true;
Object.assign(edata, {
"Error Code": e.code,
"Error Message": e.message,
Success: false
});
track("Payment Dialog: Submit", edata);
});
}
});
}
_hasError() {
return !!this._cardError;
}
_hasError() {
return !!this._cardError;
}
_calcPrice(amount, percentOff = 0, amountOff = 0) {
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
const d = Math.round((amount / 12) % 100);
_calcPrice(amount, percentOff = 0, amountOff = 0) {
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
const d = Math.round((amount / 12) % 100);
return [
`${Math.floor(amount / 1200)}`,
d < 10 ? `.0${d}` : `.${d}`
];
}
return [`${Math.floor(amount / 1200)}`, d < 10 ? `.0${d}` : `.${d}`];
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
_openSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_openSupport() {
window.open("mailto:support@padlock.io", "_system");
}
_trialExpired() {
return !this.remainingTrialDays;
}
_trialExpired() {
return !this.remainingTrialDays;
}
_hasPlan() {
return !!this.plan;
}
_hasPlan() {
return !!this.plan;
}
_submitLabel() {
return this.plan ? $l("Upgrade Now") : $l("Submit");
}
_submitLabel() {
return this.plan ? $l("Upgrade Now") : $l("Submit");
}
}
window.customElements.define(PaymentDialog.is, PaymentDialog);

View File

@ -0,0 +1,54 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import "./dialog.js";
import "./promo.js";
class PlPromoDialog extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(180deg, #555 0%, #222 100%);
};
}
</style>
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
</pl-dialog>
`;
}
static get is() {
return "pl-promo-dialog";
}
static get properties() {
return {
promo: Object
};
}
_dismiss() {
this.$.dialog.open = false;
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
_redeem() {
this.$.dialog.open = false;
typeof this._resolve === "function" && this._resolve(true);
this._resolve = null;
}
show() {
setTimeout(() => (this.$.dialog.open = true), 10);
return new Promise(resolve => {
this._resolve = resolve;
});
}
}
window.customElements.define(PlPromoDialog.is, PlPromoDialog);

View File

@ -0,0 +1,115 @@
import "../styles/shared.js";
import { localize } from "../core/locale.js";
import { BaseElement, html } from "./base.js";
import "./input.js";
import "./loading-button.js";
import "./dialog.js";
const defaultConfirmLabel = localize("OK");
const defaultCancelLabel = localize("Cancel");
const defaultType = "text";
const defaultPlaceholder = "";
class DialogPrompt extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
};
}
pl-input {
text-align: center;
}
.validation-message {
position: relative;
margin-top: 15px;
font-weight: bold;
font-size: var(--font-size-small);
color: var(--color-error);
text-shadow: none;
text-align: center;
}
</style>
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
<div class="message tiles-1" hidden\$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
<button class="tap tiles-4" on-click="_dismiss" hidden\$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
</pl-dialog>
`;
}
static get is() {
return "pl-dialog-prompt";
}
static get properties() {
return {
confirmLabel: { type: String, value: defaultConfirmLabel },
cancelLabel: { type: String, value: defaultCancelLabel },
message: { type: String, value: "" },
open: { type: Boolean, value: false },
placeholder: { type: String, value: "" },
preventDismiss: { type: Boolean, value: true },
type: { type: String, value: defaultType },
validationFn: Function,
_validationMessage: { type: String, value: "" }
};
}
_confirm() {
this.$.confirmButton.start();
const val = this.$.input.value;
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
p.then(v => {
this._validationMessage = "";
this.$.confirmButton.success();
typeof this._resolve === "function" && this._resolve(v);
this._resolve = null;
this.open = false;
}).catch(e => {
this.$.dialog.rumble();
this._validationMessage = e;
this.$.confirmButton.fail();
});
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(null);
this._resolve = null;
this.open = false;
}
_hasMessage() {
return !!this.message;
}
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
this.$.confirmButton.stop();
this.message = message || "";
this.type = type || defaultType;
this.placeholder = placeholder || defaultPlaceholder;
this.confirmLabel = confirmLabel || defaultConfirmLabel;
this.cancelLabel = cancelLabel || defaultCancelLabel;
this._hideCancelButton = cancelLabel === false;
this.preventDismiss = preventDismiss;
this.validationFn = validation;
this._validationMessage = "";
this.$.input.value = "";
this.open = true;
setTimeout(() => this.$.input.focus(), 100);
return new Promise(resolve => {
this._resolve = resolve;
});
}
}
window.customElements.define(DialogPrompt.is, DialogPrompt);

184
app/src/elements/dialog.js Normal file
View File

@ -0,0 +1,184 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { Input } from "./input.js";
import { AnimationMixin } from "../mixins";
class Dialog extends AnimationMixin(BaseElement) {
static get template() {
return html`
<style include="shared">
:host {
display: block;
@apply --fullbleed;
position: fixed;
z-index: 10;
@apply --scroll;
}
:host(:not(.open)) {
pointer-events: none;
}
.outer {
min-height: 100%;
display: flex;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
box-sizing: border-box;
}
.scrim {
display: block;
background: var(--color-background);
opacity: 0;
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
transform: translate3d(0, 0, 0);
@apply --fullbleed;
position: fixed;
}
:host(.open) .scrim {
opacity: 0.90;
}
.inner {
width: 100%;
box-sizing: border-box;
max-width: var(--pl-dialog-max-width, 400px);
z-index: 1;
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
background: var(--color-background);
color: var(--color-foreground);
border-radius: var(--border-radius);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
overflow: hidden;
@apply --pl-dialog-inner;
}
.outer {
transform: translate3d(0, 0, 0);
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
}
:host(:not(.open)) .outer {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.8);
}
</style>
<div class="scrim"></div>
<div class="outer" on-click="dismiss">
<slot name="before"></slot>
<div id="inner" class="inner" on-click="_preventDismiss">
<slot></slot>
</div>
<slot name="after"></slot>
</div>
`;
}
static get is() {
return "pl-dialog";
}
static get properties() {
return {
animationOptions: {
type: Object,
value: {
duration: 500,
fullDuration: 700
}
},
open: {
type: Boolean,
value: false,
notify: true,
observer: "_openChanged"
},
isShowing: {
type: Boolean,
value: false,
notify: true
},
preventDismiss: {
type: Boolean,
value: false
}
};
}
ready() {
super.ready();
// window.addEventListener("keydown", (e) => {
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
// this.dismiss();
// // e.preventDefault();
// // e.stopPropagation();
// }
// });
window.addEventListener("backbutton", e => {
if (this.open) {
this.dismiss();
e.preventDefault();
e.stopPropagation();
}
});
}
rumble() {
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
}
//* Changed handler for the _open_ property. Shows/hides the dialog
_openChanged() {
clearTimeout(this._hideTimeout);
// Set _display: block_ if we're showing. If we're hiding
// we need to wait until the transitions have finished before we
// set _display: none_.
if (this.open) {
if (Input.activeInput) {
Input.activeInput.blur();
}
this.style.display = "";
this.isShowing = true;
} else {
this._hideTimeout = window.setTimeout(() => {
this.style.display = "none";
this.isShowing = false;
}, 400);
}
this.offsetLeft;
this.classList.toggle("open", this.open);
this.dispatchEvent(
new CustomEvent(this.open ? "dialog-open" : "dialog-close", { bubbles: true, composed: true })
);
}
_preventDismiss(e) {
e.stopPropagation();
}
dismiss() {
if (!this.preventDismiss) {
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
this.open = false;
}
}
}
window.customElements.define(Dialog.is, Dialog);

165
app/src/elements/export.js Normal file
View File

@ -0,0 +1,165 @@
import "../styles/shared.js";
import { localize as $l } from "../core/locale.js";
import { BaseElement, html } from "./base.js";
import "./icon.js";
import { applyMixins, passwordStrength } from "../core/util.js";
import { isCordova, setClipboard } from "../core/platform.js";
import { toPadlock, toCSV } from "../core/export.js";
import { LocaleMixin, DialogMixin, DataMixin } from "../mixins";
const exportCSVWarning = $l(
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
"a secure, encrypted file, instead! Are you sure you want to proceed?"
);
class PlExport extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin) {
static get template() {
return html`
<style include="shared">
:host {
display: block;
}
.row {
display: flex;
align-items: center;
}
.label {
padding: 0 15px;
flex: 1;
}
pl-icon {
width: 50px;
height: 50px;
}
</style>
<div class="tiles tiles-1 row">
<div class="label">[[ \$l("As CSV") ]]</div>
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden\$="[[ _isMobile() ]]"></pl-icon>
</div>
<div class="tiles tiles-2 row">
<div class="label">[[ \$l("As Encrypted File") ]]</div>
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden\$="[[ _isMobile() ]]"></pl-icon>
</div>
`;
}
static get is() {
return "pl-export";
}
static get properties() {
return {
exportRecords: Array
};
}
_downloadCSV() {
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" }).then(confirm => {
if (confirm) {
setTimeout(() => {
const date = new Date().toISOString().substr(0, 10);
const fileName = `padlock-export-${date}.csv`;
const csv = toCSV(this.exportRecords);
const a = document.createElement("a");
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
a.download = fileName;
a.click();
this.dispatch("data-exported");
}, 500);
}
});
}
_copyCSV() {
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" }).then(confirm => {
if (confirm) {
setClipboard(toCSV(this.exportRecords)).then(() =>
this.alert(
$l(
"Your data has successfully been copied to the system " +
"clipboard. You can now paste it into the spreadsheet program of your choice."
)
)
);
this.dispatch("data-exported");
}
});
}
_getEncryptedData() {
return this.prompt(
$l(
"Please choose a password to protect your data. This may be the same as " +
"your master password or something else, but make sure it is sufficiently strong!"
),
$l("Enter Password"),
"password",
$l("Confirm"),
$l("Cancel")
).then(pwd => {
if (!pwd) {
if (pwd === "") {
this.alert($l("Please enter a password!"));
}
return Promise.reject();
}
if (passwordStrength(pwd).score < 2) {
return this.confirm(
$l(
"WARNING: The password you entered is weak which makes it easier for " +
"attackers to break the encryption used to protect your data. Try to use a longer " +
"password or include a variation of uppercase, lowercase and special characters as " +
"well as numbers."
),
$l("Use Anyway"),
$l("Choose Different Password"),
{ type: "warning" }
).then(confirm => {
if (!confirm) {
return Promise.reject();
}
return toPadlock(this.exportRecords, pwd);
});
} else {
return toPadlock(this.exportRecords, pwd);
}
});
}
_downloadEncrypted() {
this._getEncryptedData().then(data => {
const a = document.createElement("a");
const date = new Date().toISOString().substr(0, 10);
const fileName = `padlock-export-${date}.pls`;
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
a.download = fileName;
setTimeout(() => {
a.click();
this.dispatch("data-exported");
}, 500);
});
}
_copyEncrypted() {
this._getEncryptedData().then(data => {
setClipboard(data).then(() => {
this.alert($l("Your data has successfully been copied to the system clipboard."), { type: "success" });
});
this.dispatch("data-exported");
});
}
_isMobile() {
return isCordova();
}
}
window.customElements.define(PlExport.is, PlExport);

View File

@ -1,16 +1,15 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import '../icon/icon.js';
import '../locale/locale.js';
import '../slider/slider.js';
import '../toggle/toggle-button.js';
const { BaseElement, LocaleMixin } = padlock;
import "../styles/shared.js";
import { randomString, chars } from "../core/util.js";
import { BaseElement, html } from "./base.js";
import "./dialog.js";
import "./icon.js";
import "./slider.js";
import "./toggle-button.js";
import { LocaleMixin } from "../mixins";
class Generator extends LocaleMixin(BaseElement) {
static get template() {
return Polymer.html`
static get template() {
return html`
<style include="shared">
:host {
--pl-dialog-inner: {
@ -97,69 +96,73 @@ class Generator extends LocaleMixin(BaseElement) {
<pl-icon icon="cancel" class="close-button tap" on-click="_dismiss"></pl-icon>
</pl-dialog>
`;
}
}
static get is() { return "pl-generator"; }
static get is() {
return "pl-generator";
}
static get properties() { return {
value: {
type: String,
value: "",
notify: true
},
length: {
type: Number,
value: 10
},
lower: {
type: Boolean,
value: true
},
upper: {
type: Boolean,
value: true
},
numbers: {
type: Boolean,
value: true
},
other: {
type: Boolean,
value: false
}
}; }
static get properties() {
return {
value: {
type: String,
value: "",
notify: true
},
length: {
type: Number,
value: 10
},
lower: {
type: Boolean,
value: true
},
upper: {
type: Boolean,
value: true
},
numbers: {
type: Boolean,
value: true
},
other: {
type: Boolean,
value: false
}
};
}
static get observers() { return [
"_generate(length, lower, upper, numbers, other)"
]; }
static get observers() {
return ["_generate(length, lower, upper, numbers, other)"];
}
generate() {
this._generate();
this.$.dialog.open = true;
return new Promise((resolve) => {
this._resolve = resolve;
});
}
generate() {
this._generate();
this.$.dialog.open = true;
return new Promise(resolve => {
this._resolve = resolve;
});
}
_generate() {
var charSet = "";
this.lower && (charSet += padlock.util.chars.lower);
this.upper && (charSet += padlock.util.chars.upper);
this.numbers && (charSet += padlock.util.chars.numbers);
this.other && (charSet += padlock.util.chars.other);
_generate() {
var charSet = "";
this.lower && (charSet += chars.lower);
this.upper && (charSet += chars.upper);
this.numbers && (charSet += chars.numbers);
this.other && (charSet += chars.other);
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
}
this.value = charSet ? randomString(this.length, charSet) : "";
}
_confirm() {
typeof this._resolve === "function" && this._resolve(this.value);
this.$.dialog.open = false;
}
_confirm() {
typeof this._resolve === "function" && this._resolve(this.value);
this.$.dialog.open = false;
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(undefined);
this.$.dialog.open = false;
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(undefined);
this.$.dialog.open = false;
}
}
window.customElements.define(Generator.is, Generator);

View File

@ -1,10 +1,9 @@
import "../../styles/shared";
import { BaseElement } from "../base/base";
import { html } from "@polymer/polymer/polymer-element";
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
export class PlIcon extends BaseElement {
static get template() {
return html `
return html`
<style include="shared">
:host {
display: inline-block;
@ -218,7 +217,9 @@ export class PlIcon extends BaseElement {
`;
}
static get is() { return "pl-icon"; }
static get is() {
return "pl-icon";
}
static get properties() {
return {

258
app/src/elements/input.js Normal file
View File

@ -0,0 +1,258 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import autosize from "../../../../../node_modules/autosize/src/autosize.js";
let activeInput = null;
// On touch devices, blur active input when tapping on a non-input
document.addEventListener("touchend", () => {
if (activeInput) {
activeInput.blur();
}
});
export class Input extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
display: block;
position: relative;
}
:host(:not([multiline])) {
padding: 0 10px;
height: var(--row-height);
}
input {
box-sizing: border-box;
text-overflow: ellipsis;
}
input, textarea {
text-align: inherit;
width: 100%;
height: 100%;
min-height: inherit;
line-height: inherit;
}
::-webkit-search-cancel-button {
display: none;
}
::-webkit-input-placeholder {
text-shadow: inherit;
color: inherit;
opacity: 0.5;
@apply --pl-input-placeholder;
}
.mask {
@apply --fullbleed;
pointer-events: none;
font-size: 150%;
line-height: 22px;
letter-spacing: -4.5px;
margin-left: -4px;
}
input[disabled], textarea[disabled] {
opacity: 1;
-webkit-text-fill-color: currentColor;
}
input[invisible], textarea[invisible] {
opacity: 0;
}
</style>
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
<textarea id="input" value="{{ value::input }}" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" rows="1" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" tabindex\$="[[ _tabIndex(noTab) ]]" invisible\$="[[ _showMask(masked, value, focused) ]]" disabled\$="[[ disabled ]]"></textarea>
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled=""></textarea>
</template>
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
<input id="input" value="{{ value::input }}" tabindex\$="[[ _tabIndex(noTab) ]]" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" type\$="[[ type ]]" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" required\$="[[ required ]]" pattern\$="[[ pattern ]]" disabled\$="[[ disabled ]]" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" invisible\$="[[ _showMask(masked, value, focused) ]]">
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled="">
</template>
`;
}
static get is() {
return "pl-input";
}
static get activeInput() {
return activeInput;
}
static get properties() {
return {
autosize: {
type: Boolean,
value: false
},
autocapitalize: {
type: Boolean,
value: false
},
disabled: {
type: Boolean,
value: false
},
focused: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
readonly: true
},
invalid: {
type: Boolean,
value: false,
notifiy: true,
reflectToAttribute: true,
readonly: true
},
masked: {
type: Boolean,
value: false,
reflectToAttribute: true
},
multiline: {
type: Boolean,
value: false,
reflectToAttribute: true
},
pattern: {
type: String,
value: null
},
placeholder: {
type: String,
value: ""
},
noTab: {
type: Boolean,
value: false
},
readonly: {
type: Boolean,
value: false
},
required: {
type: Boolean,
value: ""
},
type: {
type: String,
value: "text"
},
selectOnFocus: {
type: Boolean,
value: false
},
value: {
type: String,
value: "",
notify: true,
observer: "_valueChanged"
}
};
}
get inputElement() {
return this.root.querySelector(this.multiline ? "textarea" : "input");
}
_domChange() {
if (this.autosize && this.multiline && this.inputElement) {
autosize(this.inputElement);
}
setTimeout(() => this._valueChanged(), 50);
}
_stopPropagation(e) {
e.stopPropagation();
}
_focused(e) {
e.stopPropagation();
this.focused = true;
activeInput = this;
this.dispatchEvent(new CustomEvent("focus"));
if (this.selectOnFocus) {
setTimeout(() => this.selectAll(), 10);
}
}
_blurred(e) {
e.stopPropagation();
this.focused = false;
if (activeInput === this) {
activeInput = null;
}
this.dispatchEvent(new CustomEvent("blur"));
}
_changeHandler(e) {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("change"));
}
_keydown(e) {
if (e.key === "Enter" && !this.multiline) {
this.dispatchEvent(new CustomEvent("enter"));
e.preventDefault();
e.stopPropagation();
} else if (e.key === "Escape") {
this.dispatchEvent(new CustomEvent("escape"));
e.preventDefault();
e.stopPropagation();
}
}
_valueChanged() {
this.invalid = this.inputElement && !this.inputElement.checkValidity();
if (this.autosize && this.multiline) {
autosize.update(this.inputElement);
}
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_showMask() {
return this.masked && !!this.value && !this.focused;
}
_mask(value) {
return value && value.replace(/[^\n]/g, "\u2022");
}
_computeAutoCapitalize() {
return this.autocapitalize ? "" : "off";
}
focus() {
this.inputElement.focus();
}
blur() {
this.inputElement.blur();
}
selectAll() {
try {
this.inputElement.setSelectionRange(0, this.value.length);
} catch (e) {
this.inputElement.select();
}
}
}
window.customElements.define(Input.is, Input);

View File

@ -1,29 +1,19 @@
import '../../../../../node_modules/@polymer/iron-list/iron-list.js';
import '../../styles/shared.js';
import '../animation/animation.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../export/export-dialog.js';
import '../base/base.js';
import '../icon/icon.js';
import '../input/input.js';
import '../locale/locale.js';
import '../record-item/record-item.js';
import '../sync/sync.js';
import { BaseElement, html } from "./base.js";
import { applyMixins } from "../core/util.js";
import { localize as $l } from "../core/locale.js";
import { isIOS } from "../core/platform.js";
import "@polymer/iron-list/iron-list.js";
import "../styles/shared.js";
import "./dialog-export.js";
import "./icon.js";
import "./input.js";
import "./record-item.js";
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
const { applyMixins } = padlock.util;
import { LocaleMixin, DataMixin, SyncMixin, DialogMixin, AnimationMixin } from "../mixins";
class ListView extends applyMixins(
BaseElement,
LocaleMixin,
SyncMixin,
DataMixin,
DialogMixin,
AnimationMixin
) {
static get template() {
return Polymer.html`
class ListView extends applyMixins(BaseElement, LocaleMixin, SyncMixin, DataMixin, DialogMixin, AnimationMixin) {
static get template() {
return html`
<style include="shared">
:host {
box-sizing: border-box;
@ -222,238 +212,246 @@ class ListView extends applyMixins(
<div class="rounded-corners"></div>
`;
}
}
static get is() { return "pl-list-view"; }
static get is() {
return "pl-list-view";
}
static get properties() { return {
_currentSection: {
type: String,
value: ""
},
animationOptions: {
type: Object,
value: { clear: true }
},
filterActive: {
type: Boolean,
computed: "_filterActive(filterString)"
},
multiSelect: {
type: Boolean,
value: false
},
selectedRecord: {
type: Object,
notify: true
},
selectedRecords: {
type: Array,
value: () => [],
notify: true
}
static get properties() {
return {
_currentSection: {
type: String,
value: ""
},
animationOptions: {
type: Object,
value: { clear: true }
},
filterActive: {
type: Boolean,
computed: "_filterActive(filterString)"
},
multiSelect: {
type: Boolean,
value: false
},
selectedRecord: {
type: Object,
notify: true
},
selectedRecords: {
type: Array,
value: () => [],
notify: true
}
};
}
}; }
static get observers() {
return [
"_fixScroll(records)",
"_scrollToSelected(records, selectedRecord)",
"_updateCurrentSection(records)",
"_selectedCountChanged(selectedRecords.length)"
];
}
static get observers() { return [
"_fixScroll(records)",
"_scrollToSelected(records, selectedRecord)",
"_updateCurrentSection(records)",
"_selectedCountChanged(selectedRecords.length)"
]; }
ready() {
super.ready();
window.addEventListener("keydown", e => {
switch (e.key) {
case "ArrowDown":
this.$.list.focusItem(this.$.list.firstVisibleIndex);
break;
case "ArrowUp":
this.$.list.focusItem(this.$.list.lastVisibleIndex);
break;
}
});
this.$.list.addEventListener("keydown", e => e.stopPropagation());
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
this.listen("data-loaded", () => {
this.animateRecords(600);
});
this.listen("sync-success", e => {
if (!e.detail || !e.detail.auto) {
this.animateRecords();
}
});
this.listen("data-imported", () => this.animateRecords());
}
ready() {
super.ready();
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowDown":
this.$.list.focusItem(this.$.list.firstVisibleIndex);
break;
case "ArrowUp":
this.$.list.focusItem(this.$.list.lastVisibleIndex);
break;
}
});
this.$.list.addEventListener("keydown", (e) => e.stopPropagation());
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
this.listen("data-loaded", () => { this.animateRecords(600); });
this.listen("sync-success", (e) => {
if (!e.detail || !e.detail.auto) {
this.animateRecords();
}
});
this.listen("data-imported", () => this.animateRecords());
}
dataUnloaded() {
this._clearFilter();
}
dataUnloaded() {
this._clearFilter();
}
select(record) {
this.$.list.selectItem(record);
}
select(record) {
this.$.list.selectItem(record);
}
deselect() {
this.$.list.clearSelection();
}
deselect() {
this.$.list.clearSelection();
}
recordCreated(record) {
this.select(record);
}
recordCreated(record) {
this.select(record);
}
_isEmpty() {
return !this.collection.records.filter(r => !r.removed).length;
}
_isEmpty() {
return !this.collection.records.filter((r) => !r.removed).length;
}
_openMenu() {
this.dispatchEvent(new CustomEvent("open-menu"));
}
_openMenu() {
this.dispatchEvent(new CustomEvent("open-menu"));
}
_newRecord() {
this.createRecord();
}
_newRecord() {
this.createRecord();
}
_filterActive() {
return this.filterString !== "";
}
_filterActive() {
return this.filterString !== "";
}
_clearFilter() {
this.set("filterString", "");
}
_clearFilter() {
this.set("filterString", "");
}
_toggleMenu() {
this.dispatchEvent(new CustomEvent("toggle-menu"));
}
_toggleMenu() {
this.dispatchEvent(new CustomEvent("toggle-menu"));
}
_openSettings() {
this.dispatchEvent(new CustomEvent("open-settings"));
}
_openSettings() {
this.dispatchEvent(new CustomEvent("open-settings"));
}
_openCloudView() {
this.dispatchEvent(new CustomEvent("open-cloud-view"));
}
_openCloudView() {
this.dispatchEvent(new CustomEvent("open-cloud-view"));
}
_scrollToSelected() {
const l = this.$.list;
const i = l.items.indexOf(this.selectedRecord);
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
// Scroll to item before the selected one so that selected
// item is more towards the middle of the list
l.scrollToIndex(Math.max(i - 1, 0));
}
}
_scrollToSelected() {
const l = this.$.list;
const i = l.items.indexOf(this.selectedRecord);
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
// Scroll to item before the selected one so that selected
// item is more towards the middle of the list
l.scrollToIndex(Math.max(i - 1, 0));
}
}
_fixScroll() {
// Workaround for list losing scrollability on iOS after resetting filter
isIOS().then(yes => {
if (yes) {
this.$.main.style.overflow = "hidden";
setTimeout(() => (this.$.main.style.overflow = "auto"), 100);
}
});
}
_fixScroll() {
// Workaround for list losing scrollability on iOS after resetting filter
padlock.platform.isIOS().then((yes) => {
if (yes) {
this.$.main.style.overflow = "hidden";
setTimeout(() => this.$.main.style.overflow = "auto", 100);
}
});
}
_firstInSection(records, index) {
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
}
_firstInSection(records, index) {
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
}
_lastInSection(records, index) {
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
}
_lastInSection(records, index) {
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
}
_sectionHeader(index) {
const record = this.records[index];
return index < this._recentCount
? $l("Recently Used")
: (record && record.name[0] && record.name[0].toUpperCase()) || $l("No Name");
}
_sectionHeader(index) {
const record = this.records[index];
return index < this._recentCount ? $l("Recently Used") :
record && record.name[0] && record.name[0].toUpperCase() || $l("No Name");
}
_updateCurrentSection() {
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
}
_updateCurrentSection() {
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
}
_selectSection() {
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
if (sections.length > 1) {
this.$.sectionSelector.choose("", sections).then(i => {
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
this.$.list.scrollToItem(record);
});
}
}
_selectSection() {
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
if (sections.length > 1) {
this.$.sectionSelector.choose("", sections)
.then((i) => {
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
this.$.list.scrollToItem(record);
});
}
}
animateRecords(delay = 100) {
const m4e = e => this.$.list.modelForElement(e);
animateRecords(delay = 100) {
const m4e = (e) => this.$.list.modelForElement(e);
this.$.list.style.opacity = 0;
setTimeout(() => {
const first = this.$.list.firstVisibleIndex;
const last = this.$.list.lastVisibleIndex + 1;
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
const animated = elements
.filter(el => m4e(el).index >= first && m4e(el).index <= last)
.sort((a, b) => m4e(a).index - m4e(b).index);
this.$.list.style.opacity = 0;
setTimeout(() => {
const first = this.$.list.firstVisibleIndex;
const last = this.$.list.lastVisibleIndex + 1;
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
const animated = elements
.filter((el) => m4e(el).index >= first && m4e(el).index <= last)
.sort((a, b) => m4e(a).index - m4e(b).index);
this.animateCascade(animated);
this.$.list.style.opacity = 1;
}, delay);
}
this.animateCascade(animated);
this.$.list.style.opacity = 1;
}, delay);
}
_stopPropagation(e) {
e.stopPropagation();
}
_stopPropagation(e) {
e.stopPropagation();
}
_selectedCountChanged() {
const count = this.selectedRecords && this.selectedRecords.length;
if (this._lastSelectCount && !count) {
this.multiSelect = false;
}
this._lastSelectCount = count;
}
_selectedCountChanged() {
const count = this.selectedRecords && this.selectedRecords.length;
if (this._lastSelectCount && !count) {
this.multiSelect = false;
}
this._lastSelectCount = count;
}
_recordMultiSelect() {
this.multiSelect = true;
}
_recordMultiSelect() {
this.multiSelect = true;
}
_clearMultiSelection() {
this.$.list.clearSelection();
this.multiSelect = false;
}
_clearMultiSelection() {
this.$.list.clearSelection();
this.multiSelect = false;
}
_selectAll() {
this.records.forEach(r => this.$.list.selectItem(r));
}
_selectAll() {
this.records.forEach((r) => this.$.list.selectItem(r));
}
_shareSelected() {
const exportDialog = this.getSingleton("pl-dialog-export");
exportDialog.export(this.selectedRecords);
}
_shareSelected() {
const exportDialog = this.getSingleton("pl-export-dialog");
exportDialog.export(this.selectedRecords);
}
_deleteSelected() {
this.confirm(
$l(
"Are you sure you want to delete these records? " + "This action can not be undone!",
this.selectedRecords.length
),
$l("Delete {0} Records", this.selectedRecords.length)
).then(confirmed => {
if (confirmed) {
this.deleteRecords(this.selectedRecords);
this.multiSelect = false;
}
});
}
_deleteSelected() {
this.confirm($l(
"Are you sure you want to delete these records? " +
"This action can not be undone!",
this.selectedRecords.length
), $l("Delete {0} Records", this.selectedRecords.length))
.then((confirmed) => {
if (confirmed) {
this.deleteRecords(this.selectedRecords);
this.multiSelect = false;
}
});
}
_multiSelectLabel(count) {
return count ? $l("{0} records selected", count) : $l("tap to select");
}
_multiSelectLabel(count) {
return count ? $l("{0} records selected", count) : $l("tap to select");
}
search() {
this.$.filterInput.focus();
}
search() {
this.$.filterInput.focus();
}
clearFilter() {
this.$.filterInput.value = "";
}
clearFilter() {
this.$.filterInput.value = "";
}
}
window.customElements.define(ListView.is, ListView);

View File

@ -1,10 +1,10 @@
import '../../../../../node_modules/@polymer/paper-spinner/paper-spinner-lite.js';
import '../base/base.js';
import '../icon/icon.js';
import "@polymer/paper-spinner/paper-spinner-lite.js";
import { BaseElement, html } from "./base.js";
import "./icon.js";
class LoadingButton extends padlock.BaseElement {
static get template() {
return Polymer.html`
class LoadingButton extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
display: flex;
@ -53,62 +53,66 @@ class LoadingButton extends padlock.BaseElement {
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
</button>
`;
}
}
static get is() { return "pl-loading-button"; }
static get is() {
return "pl-loading-button";
}
static get properties() { return {
_fail: {
type: Boolean,
value: false
},
_loading: {
type: Boolean,
value: false
},
_success: {
type: Boolean,
value: false
},
label: {
type: String,
value: ""
},
noTab: {
type: Boolean,
value: false
}
}; }
static get properties() {
return {
_fail: {
type: Boolean,
value: false
},
_loading: {
type: Boolean,
value: false
},
_success: {
type: Boolean,
value: false
},
label: {
type: String,
value: ""
},
noTab: {
type: Boolean,
value: false
}
};
}
start() {
clearTimeout(this._stopTimeout);
this._success = this._fail = false;
this._loading = true;
}
start() {
clearTimeout(this._stopTimeout);
this._success = this._fail = false;
this._loading = true;
}
stop() {
this._success = this._fail = this._loading = false;
}
stop() {
this._success = this._fail = this._loading = false;
}
success() {
this._loading = this._fail = false;
this._success = true;
this._stopTimeout = setTimeout(() => this.stop(), 1000);
}
success() {
this._loading = this._fail = false;
this._success = true;
this._stopTimeout = setTimeout(() => this.stop(), 1000);
}
fail() {
this._loading = this._success = false;
this._fail = true;
this._stopTimeout = setTimeout(() => this.stop(), 1000);
}
fail() {
this._loading = this._success = false;
this._fail = true;
this._stopTimeout = setTimeout(() => this.stop(), 1000);
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_buttonClass() {
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
}
_buttonClass() {
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
}
}
window.customElements.define(LoadingButton.is, LoadingButton);

View File

@ -0,0 +1,102 @@
import "../styles/shared.js";
import { BaseElement, html } from "../elements/base.js";
class Notification extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
display: block;
text-align: center;
font-weight: bold;
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
position: fixed;
left: 15px;
right: 15px;
bottom: 15px;
z-index: 10;
max-width: 400px;
margin: 0 auto;
color: var(--color-background);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
}
:host(:not(.showing)) {
transform: translateY(130%);
}
.text {
padding: 15px;
position: relative;
}
.background {
opacity: 0.95;
border-radius: var(--border-radius);
@apply --fullbleed;
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
}
:host(.error) .background, :host(.warning) .background {
background: linear-gradient(90deg, #f49300 0%, #f25b00 100%);
}
</style>
<div class="background"></div>
<div class="text" on-click="_click">{{ message }}</div>
`;
}
static get is() {
return "pl-notification";
}
static get properties() {
return {
message: String,
type: {
type: String,
value: "info",
observer: "_typeChanged"
}
};
}
show(message, type, duration) {
if (message) {
this.message = message;
}
if (type) {
this.type = type;
}
this.classList.add("showing");
if (duration) {
setTimeout(() => this.hide(false), duration);
}
return new Promise(resolve => {
this._resolve = resolve;
});
}
hide(clicked) {
this.classList.remove("showing");
typeof this._resolve === "function" && this._resolve(clicked);
this._resolve = null;
}
_typeChanged(newType, oldType) {
this.classList.remove(oldType);
this.classList.add(newType);
}
_click() {
this.hide(true);
}
}
window.customElements.define(Notification.is, Notification);

View File

@ -1,14 +1,12 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../icon/icon.js';
import '../locale/locale.js';
import "../styles/shared.js";
import { formatDateUntil, isFuture } from "../core/util.js";
import { BaseElement, html } from "./base.js";
import "./icon.js";
import { LocaleMixin } from "../mixins";
const { LocaleMixin } = padlock;
const { formatDateUntil, isFuture } = padlock.util;
class Promo extends LocaleMixin(padlock.BaseElement) {
static get template() {
return Polymer.html`
class Promo extends LocaleMixin(BaseElement) {
static get template() {
return html`
<style include="shared">
button {
@ -55,34 +53,38 @@ class Promo extends LocaleMixin(padlock.BaseElement) {
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
</button>
`;
}
}
static get is() { return "pl-promo"; }
static get is() {
return "pl-promo";
}
static get properties() { return {
promo: Object
}; }
static get properties() {
return {
promo: Object
};
}
static get observers() { return [
"_setupCountdown(promo.redeemWithin)"
]; }
static get observers() {
return ["_setupCountdown(promo.redeemWithin)"];
}
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
if (!isFuture(p.created, p.redeemWithin)) {
this.dispatchEvent(new CustomEvent("promo-expired"));
}
}, 1000);
}
}
_setupCountdown() {
clearInterval(this._countdown);
const p = this.promo;
if (p && p.redeemWithin) {
this._countdown = setInterval(() => {
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
if (!isFuture(p.created, p.redeemWithin)) {
this.dispatchEvent(new CustomEvent("promo-expired"));
}
}, 1000);
}
}
_redeem() {
this.dispatchEvent(new CustomEvent("promo-redeem"));
}
_redeem() {
this.dispatchEvent(new CustomEvent("promo-redeem"));
}
}
window.customElements.define(Promo.is, Promo);

View File

@ -1,26 +1,15 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../clipboard/clipboard.js';
import '../dialog/dialog-mixin.js';
import '../icon/icon.js';
import '../input/input.js';
import '../locale/locale.js';
import '../notification/notification.js';
import './record-field-dialog.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { applyMixins } from "../core/util";
import { isTouch } from "../core/platform";
import "./icon.js";
import "./input.js";
import "./dialog-field.js";
import { NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } from "../mixins";
const { applyMixins } = padlock.util;
const { isTouch } = padlock.platform;
const { BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } = padlock;
class RecordField extends applyMixins(
BaseElement,
NotificationMixin,
LocaleMixin,
DialogMixin,
ClipboardMixin
) {
static get template() {
return Polymer.html`
class RecordField extends applyMixins(BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin) {
static get template() {
return html`
<style include="shared">
:host {
display: block;
@ -101,88 +90,91 @@ class RecordField extends applyMixins(
<pl-icon icon="generate" class="field-button tap" on-click="_showGenerator" hidden\$="[[ _hasValue(field.value) ]]"></pl-icon>
</div>
`;
}
}
static get is() { return "pl-record-field"; }
static get is() {
return "pl-record-field";
}
static get properties() { return {
_masked: {
type: Boolean,
value: true
},
record: Object,
field: {
type: Object,
value: () => { return { name: "", value: "", masked: false }; }
}
}; }
static get properties() {
return {
_masked: {
type: Boolean,
value: true
},
record: Object,
field: {
type: Object,
value: () => {
return { name: "", value: "", masked: false };
}
}
};
}
connectedCallback() {
super.connectedCallback();
this.classList.toggle("touch", isTouch());
}
connectedCallback() {
super.connectedCallback();
this.classList.toggle("touch", isTouch());
}
_delete() {
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
}
_delete() {
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
}
_showGenerator() {
return this.generate()
.then((value) => {
this.set("field.value", value);
this.dispatchEvent(new CustomEvent("field-change"));
});
}
_showGenerator() {
return this.generate().then(value => {
this.set("field.value", value);
this.dispatchEvent(new CustomEvent("field-change"));
});
}
_copy() {
this.setClipboard(this.record, this.field);
}
_copy() {
this.setClipboard(this.record, this.field);
}
_toggleMaskIcon() {
return this.field.masked ? "show" : "hide";
}
_toggleMaskIcon() {
return this.field.masked ? "show" : "hide";
}
_toggleMask() {
this.set("field.masked", !this.field.masked);
this.dispatchEvent(new CustomEvent("field-change"));
}
_toggleMask() {
this.set("field.masked", !this.field.masked);
this.dispatchEvent(new CustomEvent("field-change"));
}
_openFieldDialog(edit, presets) {
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(this.field, edit, presets))
.then((result) => {
switch (result.action) {
case "copy":
this._copy();
break;
case "generate":
this.generate()
.then((value) => {
this._openFieldDialog(true, { name: result.name, value: value || result.value });
});
break;
case "delete":
this._delete();
break;
case "edited":
this.notifyPath("field.name");
this.notifyPath("field.value");
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
break;
}
});
}
_openFieldDialog(edit, presets) {
this.lineUpDialog("pl-dialog-field", d => d.openField(this.field, edit, presets)).then(result => {
switch (result.action) {
case "copy":
this._copy();
break;
case "generate":
this.generate().then(value => {
this._openFieldDialog(true, { name: result.name, value: value || result.value });
});
break;
case "delete":
this._delete();
break;
case "edited":
this.notifyPath("field.name");
this.notifyPath("field.value");
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
break;
}
});
}
_hasValue() {
return !!this.field.value;
}
_hasValue() {
return !!this.field.value;
}
_fieldClicked() {
this._openFieldDialog();
}
_fieldClicked() {
this._openFieldDialog();
}
_edit() {
this._openFieldDialog(true);
}
_edit() {
this._openFieldDialog(true);
}
}
window.customElements.define(RecordField.is, RecordField);

View File

@ -1,22 +1,14 @@
import '../../../../../node_modules/@polymer/polymer/lib/mixins/mutable-data.js';
import '../../styles/shared.js';
import '../base/base.js';
import '../icon/icon.js';
import '../locale/locale.js';
import '../clipboard/clipboard.js';
import "@polymer/polymer/lib/mixins/mutable-data.js";
import "../styles/shared.js";
import { applyMixins } from "../core/util.js";
import { isTouch } from "../core/platform.js";
import { BaseElement, html } from "./base.js";
import "./icon.js";
import { ClipboardMixin, LocaleMixin, DataMixin } from "../mixins";
const { ClipboardMixin, LocaleMixin, DataMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const { isTouch } = padlock.platform;
class RecordItem extends applyMixins(
BaseElement,
DataMixin,
ClipboardMixin,
LocaleMixin
) {
static get template() {
return Polymer.html`
class RecordItem extends applyMixins(BaseElement, DataMixin, ClipboardMixin, LocaleMixin) {
static get template() {
return html`
<style include="shared">
:host {
display: block;
@ -186,68 +178,72 @@ class RecordItem extends applyMixins(
</div>
`;
}
}
static get is() { return "pl-record-item"; }
static get is() {
return "pl-record-item";
}
static get properties() { return {
multiSelect: {
type: Boolean,
value: false,
reflectToAttribute: true
},
record: Object
}; }
static get properties() {
return {
multiSelect: {
type: Boolean,
value: false,
reflectToAttribute: true
},
record: Object
};
}
ready() {
super.ready();
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
// re-dispatch it
this.addEventListener("keydown", (e) => {
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
document.dispatchEvent(new KeyboardEvent("keydown", e));
e.preventDefault();
}
});
ready() {
super.ready();
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
// re-dispatch it
this.addEventListener("keydown", e => {
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
document.dispatchEvent(new KeyboardEvent("keydown", e));
e.preventDefault();
}
});
this.classList.toggle("touch", isTouch());
}
this.classList.toggle("touch", isTouch());
}
_copyField(e) {
if (!this.multiSelect) {
e.stopPropagation();
e.preventDefault();
this.setClipboard(this.record, e.model.item);
const field = this.root.querySelectorAll(".field")[e.model.index];
field.classList.add("copied");
this.record.lastUsed = new Date();
this.saveCollection();
setTimeout(() => field.classList.remove("copied"), 1000);
}
}
_copyField(e) {
if (!this.multiSelect) {
e.stopPropagation();
e.preventDefault();
this.setClipboard(this.record, e.model.item);
const field = this.root.querySelectorAll(".field")[e.model.index];
field.classList.add("copied");
this.record.lastUsed = new Date();
this.saveCollection();
setTimeout(() => field.classList.remove("copied"), 1000);
}
}
_fieldLabel(value) {
return value ? value + ":" : "";
}
_fieldLabel(value) {
return value ? value + ":" : "";
}
_hasFields() {
return !!this.record.fields.length;
}
_hasFields() {
return !!this.record.fields.length;
}
_selectIconClicked() {
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
}
_selectIconClicked() {
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
}
_limitTags() {
const tags = this.record.tags.slice(0, 2);
const more = this.record.tags.length - tags.length;
_limitTags() {
const tags = this.record.tags.slice(0, 2);
const more = this.record.tags.length - tags.length;
if (more) {
tags.push("+" + more);
}
if (more) {
tags.push("+" + more);
}
return tags;
}
return tags;
}
}
window.customElements.define(RecordItem.is, RecordItem);

View File

@ -1,26 +1,16 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../animation/animation.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../icon/icon.js';
import '../input/input.js';
import '../locale/locale.js';
import './record-field.js';
import './record-field-dialog.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { applyMixins } from "../core/util.js";
import { localize as $l } from "../core/locale.js";
import "./icon.js";
import "./input.js";
import "./record-field.js";
import "./dialog-field.js";
import { LocaleMixin, DialogMixin, DataMixin, AnimationMixin } from "../mixins";
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
class RecordView extends applyMixins(
BaseElement,
DataMixin,
LocaleMixin,
DialogMixin,
AnimationMixin
) {
static get template() {
return Polymer.html`
class RecordView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin) {
static get template() {
return html`
<style include="shared">
:host {
box-sizing: border-box;
@ -145,144 +135,145 @@ class RecordView extends applyMixins(
<div class="rounded-corners"></div>
`;
}
}
static get is() { return "pl-record-view"; }
static get is() {
return "pl-record-view";
}
static get properties() { return {
record: {
type: Object,
notify: true,
observer: "_recordChanged"
},
_edited: {
type: Boolean,
value: false
}
}; }
static get properties() {
return {
record: {
type: Object,
notify: true,
observer: "_recordChanged"
},
_edited: {
type: Boolean,
value: false
}
};
}
dataUnloaded() {
this.record = null;
const fieldDialog = this.getSingleton("pl-record-field-dialog");
fieldDialog.open = false;
fieldDialog.field = null;
}
dataUnloaded() {
this.record = null;
const fieldDialog = this.getSingleton("pl-dialog-field");
fieldDialog.open = false;
fieldDialog.field = null;
}
_recordChanged() {
setTimeout(() => {
if (this.record && !this.record.name) {
this.$.nameInput.focus();
}
}, 500);
}
_recordChanged() {
setTimeout(() => {
if (this.record && !this.record.name) {
this.$.nameInput.focus();
}
}, 500);
}
_notifyChange() {
this.dispatch("record-changed", this.record);
this.notifyPath("record");
}
_notifyChange() {
this.dispatch("record-changed", this.record);
this.notifyPath("record");
}
_deleteField(e) {
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then((confirmed) => {
if (confirmed) {
this.splice("record.fields", e.model.index, 1);
this._notifyChange();
}
});
}
_deleteField(e) {
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then(confirmed => {
if (confirmed) {
this.splice("record.fields", e.model.index, 1);
this._notifyChange();
}
});
}
_deleteRecord() {
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then((confirmed) => {
if (confirmed) {
this.deleteRecord(this.record);
}
});
}
_deleteRecord() {
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then(confirmed => {
if (confirmed) {
this.deleteRecord(this.record);
}
});
}
_addField(field = { name: "", value: "", masked: false }) {
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(field, true))
.then((result) => {
switch (result.action) {
case "generate":
this.generate()
.then((value) => {
field.value = value;
field.name = result.name;
this._addField(field);
});
break;
case "edited":
this.push("record.fields", field);
this._notifyChange();
break;
}
});
}
_addField(field = { name: "", value: "", masked: false }) {
this.lineUpDialog("pl-dialog-field", d => d.openField(field, true)).then(result => {
switch (result.action) {
case "generate":
this.generate().then(value => {
field.value = value;
field.name = result.name;
this._addField(field);
});
break;
case "edited":
this.push("record.fields", field);
this._notifyChange();
break;
}
});
}
_fieldButtonClicked() {
this._addField();
}
_fieldButtonClicked() {
this._addField();
}
_hasTags() {
return !!this.record.tags.length;
}
_hasTags() {
return !!this.record.tags.length;
}
_removeTag(e) {
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), { title: $l("Remove Tag") })
.then((confirmed) => {
if (confirmed) {
this.record.removeTag(e.model.item);
this._notifyChange();
}
});
}
_removeTag(e) {
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), {
title: $l("Remove Tag")
}).then(confirmed => {
if (confirmed) {
this.record.removeTag(e.model.item);
this._notifyChange();
}
});
}
_createTag() {
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false)
.then((tag) => {
if (tag) {
this.record.addTag(tag);
this._notifyChange();
}
});
}
_createTag() {
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false).then(tag => {
if (tag) {
this.record.addTag(tag);
this._notifyChange();
}
});
}
_addTag() {
const tags = this.collection.tags.filter((tag) => !this.record.hasTag(tag));
if (!tags.length) {
return this._createTag();
}
_addTag() {
const tags = this.collection.tags.filter(tag => !this.record.hasTag(tag));
if (!tags.length) {
return this._createTag();
}
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false })
.then((choice) => {
if (choice == tags.length) {
return this._createTag();
}
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false }).then(choice => {
if (choice == tags.length) {
return this._createTag();
}
const tag = tags[choice];
if (tag) {
this.record.addTag(tag);
this._notifyChange();
}
});
}
const tag = tags[choice];
if (tag) {
this.record.addTag(tag);
this._notifyChange();
}
});
}
_nameEnter() {
this.$.nameInput.blur();
}
_nameEnter() {
this.$.nameInput.blur();
}
animate() {
setTimeout(() => {
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
}, 100);
}
animate() {
setTimeout(() => {
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
}, 100);
}
close() {
this.dispatchEvent(new CustomEvent("record-close"));
}
close() {
this.dispatchEvent(new CustomEvent("record-close"));
}
edit() {
this.$.nameInput.focus();
}
edit() {
this.$.nameInput.focus();
}
}
window.customElements.define(RecordView.is, RecordView);

View File

@ -0,0 +1,551 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { applyMixins } from "../core/util.js";
import { getClipboard } from "../core/platform.js";
import { localize as $l } from "../core/locale.js";
import * as imp from "../core/import.js";
import "./dialog-export.js";
import "./icon.js";
import "./slider.js";
import "./toggle-button.js";
import { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin } from "../mixins";
import {
isCordova,
getReviewLink,
isTouch,
getDesktopSettings,
checkForUpdates,
saveDBAs,
loadDB,
isElectron
} from "../core/platform.js";
class SettingsView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin) {
static get template() {
return html`
<style include="shared">
@keyframes beat {
0% { transform: scale(1); }
5% { transform: scale(1.4); }
15% { transform: scale(1); }
}
:host {
@apply --fullbleed;
display: flex;
flex-direction: column;
}
main {
background: var(--color-quaternary);
}
section {
transform: translate3d(0, 0, 0);
}
section .info {
display: block;
font-size: var(--font-size-small);
text-align: center;
line-height: normal;
padding: 15px;
height: auto;
}
button {
width: 100%;
box-sizing: border-box;
}
button > pl-icon {
position: absolute;
}
pl-toggle-button {
display: block;
}
input[type="file"] {
display: none;
}
label {
line-height: 0;
}
.padlock-heart {
display: inline-block;
margin: 0 5px;
animation: beat 5s infinite;
}
.padlock-heart::before {
font-family: "FontAwesome";
content: "\\f004";
}
.made-in {
font-size: var(--font-size-tiny);
margin-top: 3px;
}
.db-path {
font-weight: bold;
margin-top: 5px;
word-wrap: break-word;
font-size: var(--font-size-tiny);
}
#customUrlInput:not([invalid]) + .url-warning {
display: none;
}
.url-warning {
font-size: var(--font-size-small);
text-align: center;
padding: 15px;
border-top: solid 1px var(--border-color);
color: var(--color-error);
}
.feature-locked {
font-size: var(--font-size-tiny);
color: var(--color-error);
margin: -14px 15px 12px 15px;
}
</style>
<header>
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
<div class="title">[[ \$l("Settings") ]]</div>
<pl-icon></pl-icon>
</header>
<main>
<section>
<div class="section-header">[[ \$l("Auto Lock") ]]</div>
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ \$l('Lock Automatically') ]]" class="tap" reverse="" on-change="settingChanged"></pl-toggle-button>
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ \$l(' min') ]]" label="[[ \$l('After') ]]" hidden\$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
</section>
<section>
<div class="section-header">[[ \$l("Synchronization") ]]</div>
<div disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ \$l('Sync Automatically') ]]" reverse="" class="tap" on-change="settingChanged"></pl-toggle-button>
<div class="feature-locked" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log in to enable auto sync!") ]]</div>
<div class="feature-locked" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
<div class="feature-locked" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
</div>
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ \$l('Use Custom Server') ]]" reverse="" on-change="_customHostChanged" class="tap" disabled\$="[[ settings.syncConnected ]]"></pl-toggle-button>
<div class="tap" hidden\$="[[ !settings.syncCustomHost ]]" disabled\$="[[ settings.syncConnected ]]">
<pl-input id="customUrlInput" placeholder="[[ \$l('Enter Custom URL') ]]" value="{{ settings.syncHostUrl }}" pattern="^https://[^\\s/\$.?#].[^\\s]*\$" required="" on-change="settingChanged"></pl-input>
<div class="url-warning">
<strong>[[ \$l("Invalid URL") ]]</strong> -
[[ \$l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
</div>
</div>
</section>
<section>
<button on-click="_changePassword" class="tap">[[ \$l("Change Master Password") ]]</button>
<button on-click="_resetData" class="tap">[[ \$l("Reset App") ]]</button>
<button class="tap" on-click="_import">[[ \$l("Import...") ]]</button>
<button class="tap" on-click="_export">[[ \$l("Export...") ]]
</button></section>
<section hidden\$="[[ !_isDesktop() ]]">
<div class="section-header">[[ \$l("Updates") ]]</div>
<pl-toggle-button id="autoUpdatesButton" label="[[ \$l('Automatically Install Updates') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
<pl-toggle-button id="betaReleasesButton" label="[[ \$l('Install Beta Releases') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
<button on-click="_checkForUpdates" class="tap">[[ \$l("Check For Updates...") ]]</button>
</section>
<section hidden\$="[[ !_isDesktop() ]]">
<div class="section-header">[[ \$l("Database") ]]</div>
<div class="info">
<div>[[ \$l("Current Location:") ]]</div>
<div class="db-path">[[ _dbPath ]]</div>
</div>
<button on-click="_saveDBAs" class="tap">[[ \$l("Change Location...") ]]</button>
<button on-click="_loadDB" class="tap">[[ \$l("Load Different Database...") ]]</button>
</section>
<section>
<button class="info tap" on-click="_openSource">
<div><strong>Padlock {{ settings.version }}</strong></div>
<div class="made-in">Made with in Germany</div>
</button>
<button on-click="_openWebsite" class="tap">[[ \$l("Website") ]]</button>
<button on-click="_sendMail" class="tap">[[ \$l("Support") ]]</button>
<button on-click="_promptReview" class="tap">
<span>[[ \$l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
</button>
</section>
</main>
<div class="rounded-corners"></div>
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden="">
`;
}
static get is() {
return "pl-settings-view";
}
connectedCallback() {
super.connectedCallback();
if (isElectron()) {
const desktopSettings = getDesktopSettings().get();
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
this._dbPath = desktopSettings.dbPath;
}
}
animate() {
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
}
_back() {
this.dispatchEvent(new CustomEvent("settings-back"));
}
//* Opens the change password dialog and resets the corresponding input elements
_changePassword() {
let newPwd;
return this.promptPassword(
this.password,
$l("Are you sure you want to change your master password? Enter your " + "current password to continue!"),
$l("Confirm"),
$l("Cancel")
)
.then(success => {
if (success) {
return this.prompt(
$l("Now choose a new master password!"),
$l("Enter New Password"),
"password",
$l("Confirm"),
$l("Cancel"),
false,
val => {
if (val === "") {
return Promise.reject($l("Please enter a password!"));
}
return Promise.resolve(val);
}
);
} else {
return Promise.reject();
}
})
.then(pwd => {
if (pwd === null) {
return Promise.reject();
}
newPwd = pwd;
return this.promptPassword(pwd, $l("Confirm your new master password!"), $l("Confirm"), $l("Cancel"));
})
.then(success => {
if (success) {
return this.setPassword(newPwd);
} else {
return Promise.reject();
}
})
.then(() => {
if (this.settings.syncConnected) {
return this.confirm(
$l(
"Do you want to update the password for you online account {0} as well?",
this.settings.syncEmail
),
$l("Yes"),
$l("No")
).then(confirmed => {
if (confirmed) {
return this.setRemotePassword(this.password);
}
});
}
})
.then(() => {
this.alert($l("Master password changed successfully."), { type: "success" });
});
}
_openWebsite() {
window.open("https://padlock.io", "_system");
}
_sendMail() {
window.open("mailto:support@padlock.io", "_system");
}
_openGithub() {
window.open("https://github.com/maklesoft/padlock/", "_system");
}
_resetData() {
this.promptPassword(
this.password,
$l(
"Are you sure you want to delete all your data and reset the app? Enter your " +
"master password to continue!"
),
$l("Reset App")
).then(success => {
if (success) {
return this.resetData();
}
});
}
_import() {
const options = [$l("From Clipboard")];
if (!isCordova()) {
options.push($l("From File"));
}
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" }).then(
choice => {
switch (choice) {
case 0:
this._importFromClipboard();
break;
case 1:
this.$.importFile.click();
break;
}
}
);
}
_importFile() {
const file = this.$.importFile.files[0];
const reader = new FileReader();
reader.onload = () => {
this._importString(reader.result).catch(e => {
switch (e.code) {
case "decryption_failed":
this.alert($l("Failed to open file. Did you enter the correct password?"), { type: "warning" });
break;
case "unsupported_container_version":
this.confirm(
$l(
"It seems the data you are trying to import was exported from a " +
"newer version of Padlock and can not be opened with the version you are " +
"currently running."
),
$l("Check For Updates"),
$l("Cancel"),
{ type: "info" }
).then(confirmed => {
if (confirmed) {
checkForUpdates();
}
});
break;
case "invalid_csv":
this.alert($l("Failed to recognize file format."), { type: "warning" });
break;
default:
this.alert($l("Failed to open file."), { type: "warning" });
throw e;
}
});
this.$.importFile.value = "";
};
reader.readAsText(file);
}
_importFromClipboard() {
getClipboard()
.then(str => this._importString(str))
.catch(e => {
switch (e.code) {
case "decryption_failed":
this.alert($l("Failed to decrypt data. Did you enter the correct password?"), {
type: "warning"
});
break;
default:
this.alert(
$l(
"No supported data found in clipboard. Please make sure to copy " +
"you data to the clipboard first (e.g. via ctrl + C)."
),
{ type: "warning" }
);
}
});
}
_importString(rawStr) {
const isPadlock = imp.isFromPadlock(rawStr);
const isSecuStore = imp.isFromSecuStore(rawStr);
const isLastPass = imp.isFromLastPass(rawStr);
const isCSV = imp.isCSV(rawStr);
return Promise.resolve()
.then(() => {
if (isPadlock || isSecuStore) {
return this.prompt(
$l("This file is protected by a password."),
$l("Enter Password"),
"password",
$l("Confirm"),
$l("Cancel")
);
}
})
.then(pwd => {
if (pwd === null) {
return;
}
if (isPadlock) {
return imp.fromPadlock(rawStr, pwd);
} else if (isSecuStore) {
return imp.fromSecuStore(rawStr, pwd);
} else if (isLastPass) {
return imp.fromLastPass(rawStr);
} else if (isCSV) {
return this.choose(
$l(
"The data you want to import seems to be in CSV format. Before you continue, " +
"please make sure that the data is structured according to Padlocks specific " +
"requirements!"
),
[$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")],
{ type: "info" }
).then(choice => {
switch (choice) {
case 0:
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
// Reopen dialog for when the user comes back from the web page
return this._importString(rawStr);
case 1:
return imp.fromCSV(rawStr);
case 2:
return;
}
});
} else {
throw new imp.ImportError("invalid_csv");
}
})
.then(records => {
if (records) {
this.addRecords(records);
this.dispatch("data-imported", { records: records });
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
}
});
}
_isMobile() {
return isCordova();
}
_isTouch() {
return isTouch();
}
_openSource() {
window.open("https://github.com/maklesoft/padlock/", "_system");
}
_autoLockInfo() {
return this.$l(
"Tell Padlock to automatically lock the app after a certain period of " +
"inactivity in case you leave your device unattended for a while."
);
}
_peekValuesInfo() {
return this.$l(
"If enabled allows peeking at field values in the record list " +
"by moving the mouse cursor over the corresponding field."
);
}
_resetDataInfo() {
return this.$l(
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
"and will restore the app to the state it was when you first launched it."
);
}
_promptReview() {
this.choose(
$l(
"So glad to hear you like our app! Would you mind taking a second to " +
"let others know what you think about Padlock?"
),
[$l("Rate Padlock"), $l("No Thanks")]
).then(choice => {
if (choice === 0) {
getReviewLink(0).then(link => window.open(link, "_system"));
}
});
}
_isDesktop() {
return isElectron();
}
_desktopSettingsChanged() {
getDesktopSettings().set({
autoDownloadUpdates: this.$.autoUpdatesButton.active,
allowPrerelease: this.$.betaReleasesButton.active
});
}
_checkForUpdates() {
checkForUpdates();
}
_saveDBAs() {
saveDBAs();
}
_loadDB() {
loadDB();
}
_export() {
const exportDialog = this.getSingleton("pl-dialog-export");
exportDialog.export(this.records);
}
_autoSyncInfoText() {
return $l(
"Enable Auto Sync to automatically synchronize your data with " +
"your Padlock online account every time you make a change!"
);
}
_customHostChanged() {
if (this.settings.syncCustomHost) {
this.confirm(
$l(
"Are you sure you want to use a custom server for synchronization? " +
"This option is only recommended for advanced users!"
),
$l("Continue")
).then(confirmed => {
if (confirmed) {
this.settingChanged();
} else {
this.set("settings.syncCustomHost", false);
}
});
} else {
this.settingChanged();
}
}
}
window.customElements.define(SettingsView.is, SettingsView);

View File

@ -1,9 +1,9 @@
import '../../styles/shared.js';
import '../base/base.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
class Slider extends padlock.BaseElement {
static get template() {
return Polymer.html`
class Slider extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
display: flex;
@ -79,58 +79,62 @@ class Slider extends padlock.BaseElement {
<input type="range" value="{{ _strValue::input }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" on-change="_inputChange">
<span class="value-display">{{ value }}{{ unit }}</span>
`;
}
}
static get is() { return "pl-slider"; }
static get is() {
return "pl-slider";
}
static get properties() { return {
min: {
type: Number,
value: 1
},
max: {
type: Number,
value: 10
},
value: {
type: Number,
observer: "_valueChanged",
notify: true
},
unit: {
type: String,
value: ""
},
step: {
type: Number,
value: 1
},
label: {
type: String,
value: ""
},
hideValue: {
type: Boolean,
value: false,
reflectToAttribute: true
},
_strValue: {
type: String,
observer: "_strValueChanged"
}
}; }
static get properties() {
return {
min: {
type: Number,
value: 1
},
max: {
type: Number,
value: 10
},
value: {
type: Number,
observer: "_valueChanged",
notify: true
},
unit: {
type: String,
value: ""
},
step: {
type: Number,
value: 1
},
label: {
type: String,
value: ""
},
hideValue: {
type: Boolean,
value: false,
reflectToAttribute: true
},
_strValue: {
type: String,
observer: "_strValueChanged"
}
};
}
_strValueChanged() {
this.value = parseFloat(this._strValue, 10);
}
_strValueChanged() {
this.value = parseFloat(this._strValue, 10);
}
_valueChanged() {
this._strValue = this.value.toString();
}
_valueChanged() {
this._strValue = this.value.toString();
}
_inputChange() {
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
_inputChange() {
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
}
window.customElements.define(Slider.is, Slider);

View File

@ -0,0 +1,882 @@
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import { applyMixins, wait, passwordStrength } from "../core/util";
import { isTouch, getAppStoreLink, checkForUpdates } from "../core/platform";
import { localize as $l } from "../core/locale.js";
import { track } from "../core/tracking.js";
import * as stats from "../core/stats.js";
import "./input.js";
import "./loading-button.js";
import { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin } from "../mixins";
class StartView extends applyMixins(BaseElement, DataMixin, LocaleMixin, DialogMixin, AnimationMixin, SyncMixin) {
static get template() {
return html`
<style include="shared">
@keyframes reveal {
from { transform: translate(0, 30px); opacity: 0; }
to { opacity: 1; }
}
@keyframes fade {
to { transform: translate(0, -200px); opacity: 0; }
}
:host {
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
@apply --fullbleed;
@apply --scroll;
color: var(--color-foreground);
display: flex;
flex-direction: column;
z-index: 5;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
transform: translate3d(0, 0, 0);
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
}
main {
@apply --fullbleed;
background: transparent;
min-height: 510px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.form-box {
width: 300px;
border-radius: 12px;
overflow: hidden;
display: flex;
margin-top: 20px;
transform: translate3d(0, 0, 0);
}
.hero {
display: block;
font-size: 110px;
height: 120px;
width: 120px;
margin-bottom: 30px;
color: rgba(255, 255, 255, 0.9);
}
.welcome-title {
font-size: 120%;
font-weight: bold;
padding: 10px;
}
.welcome-subtitle {
width: 300px;
padding: 10px;
}
.start-button {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: bold;
height: auto;
}
.start-button pl-icon {
position: relative;
top: 2px;
width: 30px;
}
.form-box pl-input, .form-box .input-wrapper {
flex: 1;
text-align: center;
}
.form-box pl-loading-button {
width: var(--row-height);
}
.strength-meter {
font-size: 12px;
font-weight: bold;
margin-top: 10px;
margin-bottom: -15px;
height: 16px;
}
.hint {
font-size: var(--font-size-tiny);
width: 305px;
margin-top: 40px;
}
.hint pl-icon {
width: 1em;
height: 1em;
vertical-align: middle;
}
:host(:not([_mode="get-started"])) .get-started,
:host(:not([_mode="unlock"])) .unlock {
display: none;
}
.get-started-steps {
width: 100%;
height: 270px;
position: relative;
}
.get-started-step {
@apply --fullbleed;
display: flex;
flex-direction: column;
align-items: center;
}
.get-started-step:not(.center) {
pointer-events: none;
}
.get-started-step > * {
transform: translate3d(0, 0, 0);
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
}
.get-started-step > :nth-child(2) {
transition-delay: 0.1s;
}
.get-started-step > :nth-child(3) {
transition-delay: 0.2s;
}
.get-started-step:not(.center) > * {
opacity: 0;
}
.get-started-step.left > * {
transform: translate3d(-200px, 0, 0);
}
.get-started-step.right > * {
transform: translate3d(200px, 0, 0);
}
.get-started-thumbs {
position: absolute;
bottom: 20px;
display: flex;
justify-content: center;
width: 100%;
}
.get-started-thumbs > * {
background: var(--color-foreground);
width: 10px;
height: 10px;
border-radius: 100%;
margin: 5px;
cursor: pointer;
}
.get-started-thumbs > .right {
opacity: 0.3;
}
button.skip {
background: none;
border: none;
height: auto;
line-height: normal;
font-weight: bold;
margin-top: 40px;
}
.version {
position: absolute;
left: 0;
right: 0;
bottom: 20px;
margin: auto;
font-size: var(--font-size-small);
color: rgba(0, 0, 0, 0.25);
text-shadow: none;
cursor: pointer;
}
.hint.choose-password {
margin-top: 30px;
width: 160px;
text-decoration: underline;
font-weight: bold;
cursor: pointer;
}
:host([open]) {
pointer-events: none;
}
:host([open]) {
transition-delay: 0.4s;
transform: translate3d(0, -100%, 0);
}
</style>
<main class="unlock">
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
<div class="form-box tiles-2 animate-in animate-out">
<pl-input id="passwordInput" type="password" class="tap" select-on-focus="" on-enter="_unlock" no-tab="[[ open ]]" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap" label="[[ \$l('Unlock') ]]" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</main>
<main class="get-started">
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
<div class="get-started-steps">
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
<div class="welcome-title animate-in">[[ \$l("Welcome to Padlock!") ]]</div>
<div class="welcome-subtitle animate-in">[[ \$l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
<div>[[ \$l("Get Started") ]]</div>
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
<div class="form-box tiles-2">
<pl-input id="emailInput" type="email" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterEmail" placeholder="[[ \$l('Enter Email Address') ]]"></pl-input>
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="cloud"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
</div>
<button class="skip" on-click="_skipEmail">[[ \$l("Use Offline") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
<div class="form-box tiles-2">
<pl-input id="codeInput" required="" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterCode" placeholder="[[ \$l('Enter Login Code') ]]"></pl-input>
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="mail"></pl-icon>
<span inner-h-t-m-l="[[ \$l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
</div>
<button class="skip" on-click="_cancelActivation">[[ \$l("Cancel") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ !_hasCloudData ]]">
<div>
<div class="form-box tiles-2">
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterCloudPassword" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ \$l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
</div>
<button class="skip" on-click="_forgotCloudPassword">[[ \$l("I Forgot My Password") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ _hasCloudData ]]">
<div>
<div class="form-box tiles-2">
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="strength-meter">[[ _pwdStrength ]]</div>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
</div>
<div on-click="_openPwdHowto" class="hint choose-password">[[ \$l("How do I choose a good master password?") ]]</div>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
<div class="form-box tiles-2">
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_confirmNewPassword" placeholder="[[ \$l('Confirm Master Password') ]]"></pl-input>
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]" <="" span="">
</span></div>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
<div class="welcome-title animate-out">[[ \$l("All done!") ]]</div>
<div class="welcome-subtitle animate-out">[[ \$l("You're all set! Enjoy using Padlock!") ]]</div>
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
<span>[[ \$l("Finish Setup") ]]</span>
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</div>
<div class="get-started-thumbs animate-in animate-out">
<div class\$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
</div>
</main>
`;
}
static get is() {
return "pl-start-view";
}
static get properties() {
return {
open: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: "_openChanged"
},
_getStartedStep: {
type: Number,
value: 0
},
_hasData: {
type: Boolean
},
_hasCloudData: {
type: Boolean,
value: false
},
_mode: {
type: String,
reflectToAttribute: true,
computed: "_computeMode(_hasData)"
},
_pwdStrength: {
type: String,
computed: "_passwordStrength(newPwd)"
}
};
}
constructor() {
super();
this._failCount = 0;
}
ready() {
super.ready();
this.dataReady()
.then(() => this.reset())
.then(() => {
if (!isTouch() && this._hasData) {
this.$.passwordInput.focus();
}
this._openChanged();
});
}
reset() {
this.$.passwordInput.value = "";
this.$.emailInput.value = "";
this.$.newPasswordInput.value = "";
this.$.confirmPasswordInput.value = "";
this.$.cloudPwdInput.value = "";
this.$.unlockButton.stop();
this.$.getStartedButton.stop();
this._failCount = 0;
this._getStartedStep = 0;
this._hasCloudData = false;
return this._checkHasData();
}
focus() {
this.$.passwordInput.focus();
}
_openChanged() {
if (this.open) {
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
animation: "fade",
duration: 400,
fullDuration: 600,
initialDelay: 0,
fill: "forwards",
easing: "cubic-bezier(1, 0, 0.2, 1)",
clear: 3000
});
} else {
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
animation: "reveal",
duration: 1000,
fullDuration: 1500,
initialDelay: 300,
fill: "backwards",
clear: 3000
});
}
}
_startSetup() {
this._getStartedStep = 1;
if (!isTouch()) {
this.$.emailInput.focus();
}
track("Setup: Start");
}
_enterEmail() {
this.$.emailInput.blur();
if (this.$.emailInput.invalid) {
this.alert(this.$.emailInput.validationMessage || $l("Please enter a valid email address!"), {
type: "warning"
}).then(() => this.$.emailInput.focus());
return;
}
this.$.emailButton.start();
stats.set({ pairingSource: "Setup" });
this.connectCloud(this.$.emailInput.value)
.then(() => {
this.$.emailButton.success();
this._getStartedStep = 2;
this.$.codeInput.value = "";
if (!isTouch()) {
this.$.codeInput.focus();
}
})
.catch(() => this.$.emailButton.fail());
track("Setup: Email", { Skipped: false, Email: this.$.emailInput.value });
}
_enterCode() {
if (this._checkingCode) {
return;
}
if (this.$.codeInput.invalid) {
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
return;
}
this._checkingCode = true;
this.$.codeButton.start();
this.activateToken(this.$.codeInput.value)
.then(success => {
if (success) {
return this.hasCloudData().then(hasData => {
this._checkingCode = false;
this.$.codeButton.success();
this._hasCloudData = hasData;
return this._connected();
});
} else {
this._checkingCode = false;
this._rumble();
this.$.codeButton.fail();
}
})
.catch(e => {
this._checkingCode = false;
this._rumble();
this.$.codeButton.fail();
this._handleCloudError(e);
});
track("Setup: Code", { Email: this.$.emailInput.value });
}
_connected() {
setTimeout(() => {
this._getStartedStep = 3;
if (!isTouch()) {
this.$.newPasswordInput.focus();
}
}, 50);
}
_cancelActivation() {
this.cancelConnect();
this.$.codeInput.value = "";
this._getStartedStep = 1;
}
_skipEmail() {
this.$.emailInput.value = "";
this._getStartedStep = 3;
if (!isTouch()) {
this.$.newPasswordInput.focus();
}
track("Setup: Email", { Skipped: true });
}
_enterNewPassword() {
this.$.newPasswordInput.blur();
const pwd = this.$.newPasswordInput.value;
if (!pwd) {
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
return;
}
const next = () => {
this._getStartedStep = 4;
if (!isTouch()) {
this.$.confirmPasswordInput.focus();
}
track("Setup: Choose Password");
};
if (passwordStrength(pwd).score < 2) {
this.choose(
$l(
"The password you entered is weak which makes it easier for attackers to break " +
"the encryption used to protect your data. Try to use a longer password or include a " +
"variation of uppercase, lowercase and special characters as well as numbers!"
),
[$l("Learn More"), $l("Choose Different Password"), $l("Use Anyway")],
{
type: "warning",
title: $l("WARNING: Weak Password"),
hideIcon: true
}
).then(choice => {
switch (choice) {
case 0:
this._openPwdHowto();
break;
case 1:
this.$.newPasswordInput.focus();
break;
case 2:
next();
break;
}
});
return;
}
next();
}
_confirmNewPassword() {
this.$.confirmPasswordInput.blur();
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
this.choose(
$l("The password you entered does not match the original one!"),
[$l("Try Again"), $l("Change Password")],
{ type: "warning" }
).then(choice => {
switch (choice) {
case 0:
this.$.confirmPasswordInput.focus();
break;
case 1:
this._getStartedStep = 3;
this.$.newPasswordInput.focus();
break;
}
});
return;
}
this._getStartedStep = 5;
track("Setup: Confirm Password");
}
_computeMode() {
return this._hasData ? "unlock" : "get-started";
}
_checkHasData() {
return this.hasData().then(has => (this._hasData = has));
}
_finishSetup() {
this._initializeData();
track("Setup: Finish");
}
_initializeData() {
this.$.getStartedButton.start();
if (this._initializing) {
return;
}
this._initializing = true;
const password = this.cloudSource.password || this.$.newPasswordInput.value;
this.cloudSource.password = password;
const promises = [this.initData(password), wait(1000)];
if (this.settings.syncConnected && !this._hasCloudData) {
promises.push(this.collection.save(this.cloudSource));
}
Promise.all(promises).then(() => {
this.$.getStartedButton.success();
this.$.newPasswordInput.blur();
this._initializing = false;
});
}
_promptResetData(message) {
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App")).then(value => {
if (value == null) {
return;
}
if (value.toUpperCase() === "RESET") {
this.resetData();
} else {
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
}
});
}
_unlock() {
const password = this.$.passwordInput.value;
if (!password) {
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
return;
}
this.$.passwordInput.blur();
this.$.unlockButton.start();
if (this._unlocking) {
return;
}
this._unlocking = true;
Promise.all([this.loadData(password), wait(1000)])
.then(() => {
this.$.unlockButton.success();
this._unlocking = false;
})
.catch(e => {
this.$.unlockButton.fail();
this._unlocking = false;
switch (e.code) {
case "decryption_failed":
this._rumble();
this._failCount++;
if (this._failCount > 2) {
this.promptForgotPassword().then(doReset => {
if (doReset) {
this.resetData();
}
});
} else {
this.$.passwordInput.focus();
}
break;
case "unsupported_container_version":
this.confirm(
$l(
"It seems the data stored on this device was saved with a newer version " +
"of Padlock and can not be opened with the version you are currently running. " +
"Please install the latest version of Padlock or reset the data to start over!"
),
$l("Check For Updates"),
$l("Reset Data")
).then(confirmed => {
if (confirmed) {
checkForUpdates();
} else {
this._promptResetData(
$l(
"Are you sure you want to reset the app? " +
"WARNING: This will delete all your data!"
)
);
}
});
break;
default:
this._promptResetData(
$l(
"An error occured while loading your data! If the problem persists, please try " +
"resetting or reinstalling the app!"
)
);
throw e;
}
});
}
_passwordStrength(pwd) {
const score = pwd ? passwordStrength(pwd).score : -1;
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
return strength && $l("strength: {0}", strength);
}
_getStartedHint(step) {
return [
$l(
"Logging in will unlock advanced features like automatic backups and seamless " +
"synchronization between all your devices!"
),
$l(
"Your **master password** is a single passphrase used to protect your data. " +
"Without it, nobody will be able to access your data - not even us!"
),
$l(
"**Don't forget your master password!** For privacy and security reasons we never store your " +
"password anywhere which means we won't be able to help you recover your data in case you forget " +
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
)
][step];
}
_getStartedClass(currStep, step) {
return currStep > step ? "left" : currStep < step ? "right" : "center";
}
_goToStep(e) {
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
if (s < this._getStartedStep) {
this._getStartedStep = s;
}
}
_openProductPage() {
getAppStoreLink().then(link => window.open(link, "_system"));
}
_openPwdHowto() {
window.open("https://padlock.io/howto/choose-master-password/", "_system");
}
_rumble() {
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`), {
animation: "rumble",
duration: 200,
clear: true
});
}
_enterCloudPassword() {
if (this._restoringCloud) {
return;
}
const password = this.$.cloudPwdInput.value;
if (!password) {
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
return;
}
this.$.cloudPwdInput.blur();
this.$.cloudPwdButton.start();
this._restoringCloud = true;
this.cloudSource.password = password;
this.collection
.fetch(this.cloudSource)
.then(() => {
this.$.cloudPwdButton.success();
this._restoringCloud = false;
this._getStartedStep = 5;
})
.catch(e => {
this.$.cloudPwdButton.fail();
this._restoringCloud = false;
if (e.code === "decryption_failed") {
this._rumble();
} else {
this._handleCloudError(e);
}
});
track("Setup: Remote Password", { Email: this.settings.syncEmail });
}
_forgotCloudPassword() {
this.forgotCloudPassword().then(() => {
this._hasCloudData = false;
this._connected();
});
}
}
window.customElements.define(StartView.is, StartView);

View File

@ -1,9 +1,9 @@
import '../../styles/shared.js';
import '../base/base.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
class TitleBar extends padlock.BaseElement {
static get template() {
return Polymer.html`
class TitleBar extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
height: var(--title-bar-height);
@ -148,22 +148,28 @@ class TitleBar extends padlock.BaseElement {
<button class="close" on-click="_close"></button>
</div>
`;
}
}
static get is() { return "pl-title-bar"; }
static get is() {
return "pl-title-bar";
}
_close() {
require("electron").remote.getCurrentWindow().close();
}
_close() {
require("electron")
.remote.getCurrentWindow()
.close();
}
_minimize() {
require("electron").remote.getCurrentWindow().minimize();
}
_minimize() {
require("electron")
.remote.getCurrentWindow()
.minimize();
}
_maximize() {
var win = require("electron").remote.getCurrentWindow();
win.setFullScreen(!win.isFullScreen());
}
_maximize() {
var win = require("electron").remote.getCurrentWindow();
win.setFullScreen(!win.isFullScreen());
}
}
window.customElements.define(TitleBar.is, TitleBar);

View File

@ -1,10 +1,10 @@
import '../../styles/shared.js';
import '../base/base.js';
import './toggle.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
import "./toggle.js";
class ToggleButton extends padlock.BaseElement {
static get template() {
return Polymer.html`
class ToggleButton extends BaseElement {
static get template() {
return html`
<style include="shared">
:host {
display: inline-block;
@ -46,31 +46,35 @@ class ToggleButton extends padlock.BaseElement {
<div>[[ label ]]</div>
</button>
`;
}
}
static get is() { return "pl-toggle-button"; }
static get is() {
return "pl-toggle-button";
}
static get properties() { return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
label: {
type: String,
value: ""
},
reverse: {
type: Boolean,
value: false,
reflectToAttribute: true
}
}; }
static get properties() {
return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
label: {
type: String,
value: ""
},
reverse: {
type: Boolean,
value: false,
reflectToAttribute: true
}
};
}
toggle() {
this.$.toggle.toggle();
}
toggle() {
this.$.toggle.toggle();
}
}
window.customElements.define(ToggleButton.is, ToggleButton);

View File

@ -1,9 +1,9 @@
import '../../styles/shared.js';
import '../base/base.js';
import "../styles/shared.js";
import { BaseElement, html } from "./base.js";
class Toggle extends padlock.BaseElement {
static get template() {
return Polymer.html`
class Toggle extends BaseElement {
static get template() {
return html`
<style include="shared"></style>
<style>
:host {
@ -47,36 +47,40 @@ class Toggle extends padlock.BaseElement {
<div class="knob"></div>
`;
}
}
static get is() { return "pl-toggle"; }
static get is() {
return "pl-toggle";
}
static get properties() { return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
notap: Boolean
}; }
static get properties() {
return {
active: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true
},
notap: Boolean
};
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this._tap.bind(this));
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this._tap.bind(this));
}
_tap() {
if (!this.notap) {
this.toggle();
}
}
_tap() {
if (!this.notap) {
this.toggle();
}
}
toggle() {
this.active = !this.active;
toggle() {
this.active = !this.active;
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
}
}
window.customElements.define(Toggle.is, Toggle);

111
app/src/mixins/analytics.js Normal file
View File

@ -0,0 +1,111 @@
import { init, track, setTrackingID } from "../core/tracking.js";
import * as statsApi from "../core/stats.js";
import { CloudSource } from "../core/source.js";
import { DataMixin } from ".";
const startedLoading = new Date().getTime();
init(new CloudSource(DataMixin.settings), statsApi);
export function AnalyticsMixin(superClass) {
return class AnalyticsMixin extends superClass {
constructor() {
super();
this.listen("data-exported", () => statsApi.set({ lastExport: new Date().getTime() }));
this.listen("settings-changed", () => {
statsApi.set({
syncCustomHost: this.settings.syncCustomHost
});
});
this.listen("data-initialized", () => {
track("Setup", {
"With Email": !!this.settings.syncEmail
});
});
this.listen("data-loaded", () => track("Unlock"));
this.listen("data-unloaded", () => track("Lock"));
this.listen("data-reset", () => {
track("Reset Local Data").then(() => setTrackingID(""));
});
this.listen("sync-connect-start", e => {
statsApi.get().then(stats => {
track("Start Pairing", {
Source: stats.pairingSource,
Email: e.detail.email
});
statsApi.set({ startedPairing: new Date().getTime() });
});
});
this.listen("sync-connect-success", () => {
return statsApi.get().then(stats => {
track("Finish Pairing", {
Success: true,
Source: stats.pairingSource,
$duration: (new Date().getTime() - stats.startedPairing) / 1000
});
});
});
this.listen("sync-connect-cancel", () => {
return statsApi.get().then(stats => {
track("Finish Pairing", {
Success: false,
Canceled: true,
Source: stats.pairingSource,
$duration: (new Date().getTime() - stats.startedPairing) / 1000
});
});
});
this.listen("sync-disconnect", () => track("Unpair").then(() => setTrackingID("")));
let startedSync;
this.listen("sync-start", () => (startedSync = new Date().getTime()));
this.listen("sync-success", e => {
statsApi.set({ lastSync: new Date().getTime() }).then(() =>
track("Synchronize", {
Success: true,
"Auto Sync": e.detail ? e.detail.auto : false,
$duration: (new Date().getTime() - startedSync) / 1000
})
);
});
this.listen("sync-fail", e =>
track("Synchronize", {
Success: false,
"Auto Sync": e.detail.auto,
"Error Code": e.detail.error.code,
$duration: (new Date().getTime() - startedSync) / 1000
})
);
}
connectedCallback() {
super.connectedCallback();
this.hasData().then(hasData =>
track("Launch", {
"Clean Launch": !hasData,
$duration: (new Date().getTime() - startedLoading) / 1000
})
);
}
track(event, props) {
// Don't track events if user is using a custom padlock cloud instance
if (DataMixin.settings.syncCustomHost) {
return Promise.resolve();
}
track(event, props);
}
};
}

View File

@ -1,5 +1,3 @@
import '../base/base.js';
const defaults = {
animation: "slideIn",
duration: 500,
@ -12,35 +10,40 @@ const defaults = {
direction: "normal"
};
padlock.AnimationMixin = (superClass) => {
export function AnimationMixin(superClass) {
return class AnimationMixin extends superClass {
static get properties() { return {
animationOptions: {
type: Object,
value: () => { return {}; }
}
}; }
static get properties() {
return {
animationOptions: {
type: Object,
value: () => {
return {};
}
}
};
}
animateElement(el, opts = {}) {
const { animation, duration, direction, easing, delay, fill, clear } =
Object.assign({}, defaults, this.animationOptions, opts);
const { animation, duration, direction, easing, delay, fill, clear } = Object.assign(
{},
defaults,
this.animationOptions,
opts
);
clearTimeout(el.clearAnimation);
el.style.animation = "";
el.offsetLeft;
el.style.animation = `${animation} ${direction} ${duration}ms ${easing} ${delay}ms ${fill}`;
if (clear) {
const clearDelay = typeof clear === "number" ? clear : 0;
el.clearAnimation = setTimeout(() => el.style.animation = "", delay + duration + clearDelay);
el.clearAnimation = setTimeout(() => (el.style.animation = ""), delay + duration + clearDelay);
}
return new Promise((resolve) => setTimeout(resolve, delay + duration));
return new Promise(resolve => setTimeout(resolve, delay + duration));
}
animateCascade(els, opts = {}) {
const { fullDuration, duration, initialDelay } =
Object.assign({}, defaults, this.animationOptions, opts);
const { fullDuration, duration, initialDelay } = Object.assign({}, defaults, this.animationOptions, opts);
const dt = Math.max(30, Math.floor((fullDuration - duration) / els.length));
const promises = [];
@ -50,6 +53,5 @@ padlock.AnimationMixin = (superClass) => {
return Promise.all(promises);
}
};
};
}

View File

@ -1,13 +1,12 @@
import '../base/base.js';
padlock.AutoLockMixin = (superClass) => {
import { debounce } from "../core/util.js";
import { localize as $l } from "../core/locale.js";
export function AutoLockMixin(superClass) {
return class AutoLockMixin extends superClass {
constructor() {
super();
const moved = padlock.util.debounce(() => this._autoLockChanged(), 300);
const moved = debounce(() => this._autoLockChanged(), 300);
document.addEventListener("touchstart", moved, { passive: true });
document.addEventListener("keydown", moved);
document.addEventListener("mousemove", moved);
@ -16,9 +15,9 @@ padlock.AutoLockMixin = (superClass) => {
document.addEventListener("resume", () => this._resume());
}
static get observers() { return [
"_autoLockChanged(settings.autoLock, settings.autoLockDelay, locked, isSynching)"
]; }
static get observers() {
return ["_autoLockChanged(settings.autoLock, settings.autoLockDelay, locked, isSynching)"];
}
get lockDelay() {
return this.settings.autoLockDelay * 60 * 1000;
@ -71,7 +70,5 @@ padlock.AutoLockMixin = (superClass) => {
}, this.lockDelay - 10000);
}
}
};
};
}

View File

@ -1,12 +1,10 @@
import '../base/base.js';
padlock.AutoSyncMixin = (superClass) => {
import { debounce } from "../core/util.js";
export function AutoSyncMixin(superClass) {
return class AutoSyncMixin extends superClass {
constructor() {
super();
const debouncedSynchronize = padlock.util.debounce(() => this.synchronize(true), 1000);
const debouncedSynchronize = debounce(() => this.synchronize(true), 1000);
const autoSync = () => {
if (this.settings.syncAuto && this.settings.syncConnected) {
debouncedSynchronize();
@ -17,5 +15,4 @@ padlock.AutoSyncMixin = (superClass) => {
this.listen("data-loaded", autoSync);
}
};
};
}

View File

@ -0,0 +1,23 @@
import "../elements/clipboard";
let clipboardSingleton;
export function ClipboardMixin(baseClass) {
return class ClipboardMixin extends baseClass {
setClipboard(record, field, duration) {
if (!clipboardSingleton) {
clipboardSingleton = document.createElement("pl-clipboard");
document.body.appendChild(clipboardSingleton);
clipboardSingleton.offsetLeft;
}
return clipboardSingleton.set(record, field, duration);
}
clearClipboard() {
if (clipboardSingleton) {
clipboardSingleton.clear();
}
}
};
}

View File

@ -1,16 +1,16 @@
import "../base/base";
import { MutableData } from "@polymer/polymer/lib/mixins/mutable-data";
import { localize as $l } from "../locale/locale";
import { Collection, Record, Settings } from "../../core/data";
import { FileSource, EncryptedSource, LocalStorageSource } from "../../core/source";
import { getDesktopSettings } from "../../core/platform";
import { debounce } from "../../core/util";
import { localize as $l } from "../core/locale.js";
import { Collection, Record, Settings } from "../core/data.js";
import { FileSource, EncryptedSource, LocalStorageSource } from "../core/source.js";
import { getDesktopSettings } from "../core/platform.js";
import { debounce } from "../core/util.js";
export const collection = new Collection();
export const settings = new Settings();
const desktopSettings = getDesktopSettings();
const dbPath = desktopSettings ? desktopSettings.get("dbPath") : "data.pls";
const collection = new Collection();
const localSource = new EncryptedSource(new FileSource(dbPath));
const settings = new Settings();
const settingsSource = new EncryptedSource(new FileSource("settings.pls"));
const dispatcher = document.createElement("div");
@ -280,5 +280,3 @@ Object.assign(DataMixin, {
collection,
settings
});
padlock.DataMixin = DataMixin;

View File

@ -1,20 +1,17 @@
import '../base/base.js';
import '../generator/generator.js';
import '../locale/locale.js';
import './dialog-alert.js';
import './dialog-confirm.js';
import './dialog-prompt.js';
import './dialog-options.js';
import { localize as $l } from "../core/locale";
import "../elements/generator.js";
import "../elements/dialog-alert.js";
import "../elements/dialog-confirm.js";
import "../elements/dialog-prompt.js";
import "../elements/dialog-options.js";
const dialogElements = {};
let lastDialogPromise = Promise.resolve();
let currentDialog;
padlock.DialogMixin = (superClass) => {
export function DialogMixin(superClass) {
return class DialogMixin extends superClass {
getDialog(elName) {
let el = dialogElements[elName];
@ -28,11 +25,10 @@ padlock.DialogMixin = (superClass) => {
lineUpDialog(dialog, fn) {
dialog = typeof dialog === "string" ? this.getDialog(dialog) : dialog;
const promise = lastDialogPromise
.then(() => {
currentDialog = dialog;
return fn(dialog);
});
const promise = lastDialogPromise.then(() => {
currentDialog = dialog;
return fn(dialog);
});
lastDialogPromise = promise;
@ -40,19 +36,16 @@ padlock.DialogMixin = (superClass) => {
}
alert(message, options) {
return this.lineUpDialog("pl-dialog-alert", (dialog) => dialog.show(message, options));
return this.lineUpDialog("pl-dialog-alert", dialog => dialog.show(message, options));
}
confirm(message, confirmLabel = $l("Confirm"), cancelLabel = $l("Cancel"), options = { type: "question" }) {
options.options = [
confirmLabel,
cancelLabel
];
return this.alert(message, options).then((choice) => choice === 0);
options.options = [confirmLabel, cancelLabel];
return this.alert(message, options).then(choice => choice === 0);
}
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss, verify) {
return this.lineUpDialog("pl-dialog-prompt", (dialog) => {
return this.lineUpDialog("pl-dialog-prompt", dialog => {
return dialog.prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss, verify);
});
}
@ -63,7 +56,7 @@ padlock.DialogMixin = (superClass) => {
}
generate() {
return this.lineUpDialog("pl-generator", (dialog) => dialog.generate());
return this.lineUpDialog("pl-generator", dialog => dialog.generate());
}
getSingleton(elName) {
@ -78,7 +71,7 @@ padlock.DialogMixin = (superClass) => {
}
promptPassword(password, msg, confirmLabel, cancelLabel) {
return this.prompt(msg, $l("Enter Password"), "password", confirmLabel, cancelLabel, true, (pwd) => {
return this.prompt(msg, $l("Enter Password"), "password", confirmLabel, cancelLabel, true, pwd => {
if (!pwd) {
return Promise.reject($l("Please enter a password!"));
} else if (pwd !== password) {
@ -90,17 +83,25 @@ padlock.DialogMixin = (superClass) => {
}
promptForgotPassword() {
return this.confirm($l(
"For security reasons don't keep a record of your master password so unfortunately we cannot " +
"help you recover it. You can reset your password, but your data will be lost in the process!"
), $l("Reset Password"), $l("Keep Trying"), { hideIcon: true, title: $l("Forgot Your Password?") })
.then((confirmed) => {
return confirmed && this.confirm($l(
"Are you sure you want to reset your password? " +
"WARNING: All your data will be lost!"
), $l("Reset Password"), $l("Cancel"), { type: "warning" });
});
return this.confirm(
$l(
"For security reasons don't keep a record of your master password so unfortunately we cannot " +
"help you recover it. You can reset your password, but your data will be lost in the process!"
),
$l("Reset Password"),
$l("Keep Trying"),
{ hideIcon: true, title: $l("Forgot Your Password?") }
).then(confirmed => {
return (
confirmed &&
this.confirm(
$l("Are you sure you want to reset your password? " + "WARNING: All your data will be lost!"),
$l("Reset Password"),
$l("Cancel"),
{ type: "warning" }
)
);
});
}
};
};
}

262
app/src/mixins/hints.js Normal file
View File

@ -0,0 +1,262 @@
import { localize as $l } from "../core/locale";
import { wait } from "../core/util";
import { isChromeApp, isChromeOS, getReviewLink } from "../core/platform";
import * as statsApi from "../core/stats";
const day = 1000 * 60 * 60 * 24;
let stats;
const statsLoaded = statsApi.get().then(s => (stats = s));
function daysPassed(date) {
return (new Date().getTime() - date) / day;
}
export function HintsMixin(superClass) {
return class HintsMixin extends superClass {
constructor() {
super();
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindBackup()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindSync()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._askFeedback()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._crossPlatformHint()));
this.listen("auto-lock", () => statsLoaded.then(() => this._showAutoLockNotice()));
this.listen("settings-changed", () => this._notifySubStatus());
this.listen("sync-connect-success", () => this._notifySubStatus());
this.listen("data-loaded", () => wait(1000).then(() => this._notifySubStatus()));
}
ready() {
super.ready();
statsLoaded.then(() => wait(1000)).then(() => this._notifyChromeAppDeprecation());
}
_remindBackup() {
if (
daysPassed(stats.lastExport || stats.firstLaunch) > 7 &&
daysPassed(stats.lastBackupReminder || stats.firstLaunch) > 7 &&
!this.settings.syncConnected
) {
this.choose(
$l(
"Have you backed up your data yet? Remember that by default your data is only stored " +
"locally and can not be recovered in case your device gets damaged or lost. " +
"Log in now to enable automatic online backups!"
),
[$l("Log In"), $l("Do Nothing")],
{ type: "warning" }
).then(choice => {
switch (choice) {
case 0:
statsApi.set({ pairingSource: "Backup Reminder" });
this._openCloudView();
break;
}
});
statsApi.set({ lastBackupReminder: new Date().getTime() });
}
}
_remindSync() {
const daysSinceLastSync = daysPassed(stats.lastSync || stats.firstLaunch);
if (
this.settings.syncConnected &&
daysSinceLastSync > 7 &&
daysPassed(stats.lastSyncReminder || stats.firstLaunch) > 7
) {
this.choose(
$l(
"The last time you synced your data with your account was {0} days ago! You should " +
"synchronize regularly to keep all your devices up-to-date and to make sure you always " +
"have a recent backup in the cloud.",
Math.floor(daysSinceLastSync)
),
[$l("Synchronize Now"), $l("Turn On Auto Sync"), $l("Do Nothing")],
{ type: "warning" }
).then(choice => {
switch (choice) {
case 0:
this.synchronize();
break;
case 1:
this.settings.syncAuto = true;
this.dispatch("settings-changed");
this.synchronize();
break;
}
});
statsApi.set({ lastSyncReminder: new Date().getTime() });
}
}
_notifyChromeAppDeprecation() {
isChromeOS().then(yes => {
if (isChromeApp() && !yes) {
this.confirm(
$l(
"In August 2016, Google announced that they will be discontinuing Chrome Apps for all " +
"operating systems other than ChromeOS. This means that by early 2018, you will no " +
"longer be able to load this app! Don't worry though, because Padlock is now " +
"available as a native app for Windows, MacOS and Linux! Head over to our website " +
"to find out how to switch now!"
),
$l("Learn More"),
$l("Dismiss"),
{ type: "info", hideIcon: true }
).then(confirmed => {
if (confirmed) {
window.open("https://padlock.io/news/discontinuing-chrome/", "_system");
}
});
}
});
}
_askFeedback() {
if (stats.launchCount > 20 && !stats.dontAskFeeback && !stats.lastAskedFeedback) {
this.choose(
$l("Hey there! Sorry to bother you, but we'd love to know how you are liking Padlock so far!"),
[$l("I Love it!") + " \u2665", $l("It's not bad, but..."), $l("Hate it.")]
).then(rating => {
this.track("Rate App", { Rating: rating });
statsApi.set({
lastAskedFeedback: new Date().getTime(),
lastRating: rating,
lastRatedVersion: this.settings.version
});
switch (rating) {
case 0:
this.choose(
$l(
"So glad to hear you like our app! Would you mind taking a second to " +
"let others know what you think about Padlock?"
),
[$l("Rate Padlock"), $l("No Thanks")]
).then(choice => {
switch (choice) {
case 0:
this.track("Review App", { Rating: rating });
this._sendFeedback(rating);
statsApi.set({ lastReviewed: new Date().getTime() });
break;
case 1:
statsApi.set({ dontAskFeedback: true });
break;
}
});
break;
case 1:
case 2:
this.choose(
$l(
"Your opinion as a user is very important to us and we're always " +
"looking for feedback and suggestions on how to improve Padlock. " +
"Would you mind taking a second to let us know what you think about " +
"the app?"
),
[$l("Send Feedback"), $l("No Thanks")]
).then(choice => {
if (choice === 0) {
this.track("Provide Feedback", { Rating: rating });
this._sendFeedback(rating);
}
});
break;
}
});
}
}
_sendFeedback(rating) {
getReviewLink(rating).then(link => window.open(link, "_system"));
}
_showAutoLockNotice() {
if (!stats.hasShownAutoLockNotice) {
const minutes = this.settings.autoLockDelay;
setTimeout(() => {
this.alert(
$l(
"Padlock was automatically locked after {0} {1} " +
"of inactivity. You can change this behavior from the settings page.",
minutes,
minutes > 1 ? $l("minutes") : $l("minute")
)
);
}, 1000);
statsApi.set({ hasShownAutoLockNotice: true });
}
}
_notifySubStatus() {
if (this.promo && !this._hasShownPromo) {
this.alertPromo();
this._hasShownPromo = true;
return;
}
if (
this.settings.syncSubStatus === "trialing" &&
(!stats.lastTrialEndsReminder || daysPassed(stats.lastTrialEndsReminder) > 7)
) {
this.buySubscription("App - Trialing (Alert)");
statsApi.set({ lastTrialEndsReminder: new Date().getTime() });
return;
}
if (this.settings.syncConnected && this.settings.syncSubStatus !== this._lastSubStatus) {
this._lastSubStatus = this.settings.syncSubStatus;
if (this.isTrialExpired()) {
this.alert(this.trialExpiredMessage(), {
type: "warning",
title: $l("Trial Expired"),
options: [$l("Upgrade Now")]
}).then(choice => {
if (choice === 0) {
this.buySubscription("App - Trial Expired (Alert)");
}
});
} else if (this.isSubUnpaid()) {
this.alert(this.subUnpaidMessage(), {
type: "warning",
title: $l("Payment Failed"),
options: [$l("Update Payment Method"), $l("Contact Support")]
}).then(choice => {
if (choice === 0) {
this.updatePaymentMethod("App - Payment Failed (Alert)");
} else if (choice === 1) {
window.open("mailto:support@padlock.io", "_system");
}
});
}
}
}
_crossPlatformHint() {
if (!this.settings.syncConnected && !stats.hasShownCrossPlatformHint) {
this.confirm(
$l(
"Padlock lets you share your data seamlessly across all your devices like phones, " +
"tablets and computers! Log in now and we'll get you set up in just few steps!"
),
$l("Log In"),
$l("No Thanks"),
{ title: $l("Did You Know?"), type: "question" }
).then(confirmed => {
if (confirmed) {
statsApi.set({ pairingSource: "Cross-Platform Hint" });
this._openCloudView();
}
});
statsApi.set({ hasShownCrossPlatformHint: new Date().getTime() });
}
}
};
}

13
app/src/mixins/index.js Normal file
View File

@ -0,0 +1,13 @@
export { AnalyticsMixin } from "./analytics.js";
export { AnimationMixin } from "./animation.js";
export { AutoLockMixin } from "./auto-lock.js";
export { AutoSyncMixin } from "./auto-sync.js";
export { DataMixin } from "./data.js";
export { DialogMixin } from "./dialog.js";
export { HintsMixin } from "./hints.js";
export { LocaleMixin } from "./locale.js";
export { MessagesMixin } from "./messages.js";
export { SubInfoMixin } from "./subinfo.js";
export { SyncMixin } from "./sync.js";
export { NotificationMixin } from "./notification.js";
export { ClipboardMixin } from "./clipboard.js";

9
app/src/mixins/locale.js Normal file
View File

@ -0,0 +1,9 @@
import { localize } from "../core/locale";
export function LocaleMixin(superClass) {
return class LocaleMixin extends superClass {
$l() {
return localize.apply(null, arguments);
}
};
}

View File

@ -0,0 +1,37 @@
import { localize as $l } from "../core/locale.js";
import { wait } from "../core/util.js";
import { Messages } from "../core/messages.js";
import { FileSource } from "../core/source.js";
export function MessagesMixin(superClass) {
return class MessagesMixin extends superClass {
ready() {
super.ready();
this._messages = new Messages(
"https://padlock.io/messages.json",
new FileSource("read-messages.json"),
this.settings
);
this.listen("data-loaded", () => this.checkMessages());
}
checkMessages() {
wait(1000)
.then(() => this._messages.fetch())
.then(aa => aa.forEach(a => this._displayMessage(a)));
}
_displayMessage(a) {
if (a.link) {
this.confirm(a.text, $l("Learn More"), $l("Dismiss"), { type: "info" }).then(confirmed => {
if (confirmed) {
window.open(a.link, "_system");
}
this._messages.markRead(a);
});
} else {
this.alert(a.text).then(() => this._messages.markRead(a));
}
}
};
}

View File

@ -0,0 +1,17 @@
import "../elements/notification.js";
let notificationSingleton;
export function NotificationMixin(baseClass) {
return class NotificationMixin extends baseClass {
notify(message, type, duration) {
if (!notificationSingleton) {
notificationSingleton = document.createElement("pl-notification");
document.body.appendChild(notificationSingleton);
notificationSingleton.offsetLeft;
}
return notificationSingleton.show(message, type, duration);
}
};
}

View File

@ -1,26 +1,25 @@
import '../base/base.js';
const { isFuture } = padlock.util;
padlock.SubInfoMixin = (superClass) => {
import { isFuture } from "../core/util";
import { localize as $l } from "../core/locale.js";
export function SubInfoMixin(superClass) {
return class SyncMixin extends superClass {
static get properties() { return {
remainingTrialDays: {
type: Number,
computed: "_computeRemainingTrialDays(account.subscription.trialEnd)"
},
promo: {
type: Object,
computed: "_getPromo(account.promo, subStatus)"
},
subStatus: {
type: String,
computed: "identity(account.subscription.status)",
value: ""
}
}; }
static get properties() {
return {
remainingTrialDays: {
type: Number,
computed: "_computeRemainingTrialDays(account.subscription.trialEnd)"
},
promo: {
type: Object,
computed: "_getPromo(account.promo, subStatus)"
},
subStatus: {
type: String,
computed: "identity(account.subscription.status)",
value: ""
}
};
}
isTrialing() {
return this.subStatus === "trialing";
@ -51,7 +50,7 @@ padlock.SubInfoMixin = (superClass) => {
trialingMessage() {
return $l(
"Your trial period ends in {0} days. Upgrade now to continue using online features like " +
"synchronization and automatic backups!",
"synchronization and automatic backups!",
this.remainingTrialDays
);
}
@ -59,21 +58,20 @@ padlock.SubInfoMixin = (superClass) => {
trialExpiredMessage() {
return $l(
"Your free trial has expired. Upgrade now to continue using advanced features like " +
"automatic online backups and seamless synchronization between devices!"
"automatic online backups and seamless synchronization between devices!"
);
}
subUnpaidMessage() {
return $l(
"Your last payment has failed. Please contact your card provider " +
"or update your payment method!"
"Your last payment has failed. Please contact your card provider " + "or update your payment method!"
);
}
subCanceledMessage() {
return $l(
"Your subscription has been canceled. Reactivate it now to continue using advanced " +
"features like automatic online backups and seamless synchronization between devices!"
"features like automatic online backups and seamless synchronization between devices!"
);
}
@ -85,13 +83,9 @@ padlock.SubInfoMixin = (superClass) => {
_getPromo() {
const promo = this.account && this.account.promo;
const promoActive = !this.isSubActive() && promo && (
!promo.redeemWithin ||
isFuture(promo.created, promo.redeemWithin)
);
const promoActive =
!this.isSubActive() && promo && (!promo.redeemWithin || isFuture(promo.created, promo.redeemWithin));
return promoActive ? promo : null;
}
};
};
}

View File

@ -1,31 +1,31 @@
import '../base/base.js';
import '../data/data.js';
import '../payment-dialog/payment-dialog.js';
import '../promo/promo-dialog.js';
import './subinfo.js';
import { EncryptedSource, CloudSource } from "../core/source.js";
import { formatDateFromNow } from "../core/util.js";
import { settings } from "./data.js";
import { localize as $l } from "../core/locale.js";
import * as statsApi from "../core/stats.js";
import { checkForUpdates } from "../core/platform.js";
import { SubInfoMixin } from ".";
import "../elements/dialog-payment.js";
import "../elements/dialog-promo.js";
const { EncryptedSource, CloudSource } = padlock.source;
const { DataMixin, SubInfoMixin } = padlock;
const { formatDateFromNow } = padlock.util;
const cloudSource = new EncryptedSource(new CloudSource(DataMixin.settings));
padlock.SyncMixin = (superClass) => {
const cloudSource = new EncryptedSource(new CloudSource(settings));
export function SyncMixin(superClass) {
return class SyncMixin extends SubInfoMixin(superClass) {
static get properties() { return {
account: {
type: Object,
computed: "identity(settings.account)"
},
isSynching: {
type: Boolean,
value: false,
notify: true
},
lastSync: String
}; }
static get properties() {
return {
account: {
type: Object,
computed: "identity(settings.account)"
},
isSynching: {
type: Boolean,
value: false,
notify: true
},
lastSync: String
};
}
get cloudSource() {
return cloudSource;
@ -33,8 +33,8 @@ padlock.SyncMixin = (superClass) => {
constructor() {
super();
this.listen("data-unloaded", () => cloudSource.password = "");
this.listen("data-reset", () => cloudSource.password = "");
this.listen("data-unloaded", () => (cloudSource.password = ""));
this.listen("data-reset", () => (cloudSource.password = ""));
this.listen("sync-start", () => this._syncStart());
this.listen("sync-success", () => this._syncSuccess());
this.listen("sync-fail", () => this._syncFail());
@ -43,22 +43,22 @@ padlock.SyncMixin = (superClass) => {
}
connectCloud(email) {
return this._requestAuthToken(email.toLowerCase(), false, "code")
.then(() => this.dispatch("sync-connect-start", { email: email }));
return this._requestAuthToken(email.toLowerCase(), false, "code").then(() =>
this.dispatch("sync-connect-start", { email: email })
);
}
activateToken(code) {
return cloudSource.source.activateToken(code)
.then((success) => {
this.settings.syncConnected = success;
this.settings.syncAuto = true;
this.dispatch("settings-changed");
return cloudSource.source.activateToken(code).then(success => {
this.settings.syncConnected = success;
this.settings.syncAuto = true;
this.dispatch("settings-changed");
if (success) {
this.dispatch("sync-connect-success");
}
return success;
});
if (success) {
this.dispatch("sync-connect-success");
}
return success;
});
}
cancelConnect() {
@ -79,13 +79,14 @@ padlock.SyncMixin = (superClass) => {
this.dispatch("sync-disconnect");
};
this.cloudSource.source.logout()
this.cloudSource.source
.logout()
.then(done)
.catch(done);
}
synchronize(auto) {
if (!this.settings.syncConnected || auto === true && !this.isSubValid()) {
if (!this.settings.syncConnected || (auto === true && !this.isSubValid())) {
return;
}
@ -97,27 +98,30 @@ padlock.SyncMixin = (superClass) => {
if (this._currentSync) {
// There is already a synchronization in process. wait for the current sync to finish
// before starting a new one.
const chained = this._chainedSync = this._currentSync
.then(() => {
this._chainedSync = null;
return this.synchronize();
});
const chained = (this._chainedSync = this._currentSync.then(() => {
this._chainedSync = null;
return this.synchronize();
}));
return chained;
}
const sync = this._currentSync = this._synchronize(auto)
.then(() => this._currentSync = null, () => this._currentSync = null);
const sync = (this._currentSync = this._synchronize(auto).then(
() => (this._currentSync = null),
() => (this._currentSync = null)
));
return sync;
}
setRemotePassword(password) {
this.dispatch("sync-start");
cloudSource.password = password;
return this.collection.save(cloudSource)
return this.collection
.save(cloudSource)
.then(() => {
this.dispatch("sync-success");
this.alert($l("Successfully updated the password for your account {0}.", this.settings.syncEmail),
{ type: "success" });
this.alert($l("Successfully updated the password for your account {0}.", this.settings.syncEmail), {
type: "success"
});
})
.catch(() => {
this.dispatch("sync-fail");
@ -138,35 +142,39 @@ padlock.SyncMixin = (superClass) => {
return Promise.reject();
}
this._testCredsPromise = this._testCredsPromise || cloudSource.source.testCredentials()
.then((connected) => {
this._testCredsPromise = null;
this.settings.syncConnected = connected;
this.settings.syncAuto = true;
this.dispatch("settings-changed");
this._testCredsPromise =
this._testCredsPromise ||
cloudSource.source
.testCredentials()
.then(connected => {
this._testCredsPromise = null;
this.settings.syncConnected = connected;
this.settings.syncAuto = true;
this.dispatch("settings-changed");
if (connected) {
this.dispatch("sync-connect-success");
if (connected) {
this.dispatch("sync-connect-success");
this.confirm(
$l("You successfully paired this device with Padlock Cloud!"),
$l("Synchonize Now"), $l("Dismiss")
).then((confirmed) => {
if (confirmed) {
this.synchronize();
}
});
}
this.confirm(
$l("You successfully paired this device with Padlock Cloud!"),
$l("Synchonize Now"),
$l("Dismiss")
).then(confirmed => {
if (confirmed) {
this.synchronize();
}
});
}
if (!connected && pollInterval) {
setTimeout(() => this.testCredentials(pollInterval), pollInterval);
}
})
.catch((e) => {
this._testCredsPromise = null;
this.cancelConnect();
this._handleCloudError(e);
});
if (!connected && pollInterval) {
setTimeout(() => this.testCredentials(pollInterval), pollInterval);
}
})
.catch(e => {
this._testCredsPromise = null;
this.cancelConnect();
this._handleCloudError(e);
});
return this._testCredsPromise;
}
@ -174,8 +182,9 @@ padlock.SyncMixin = (superClass) => {
openDashboard(action = "", referer = "app") {
action = encodeURIComponent(action);
referer = encodeURIComponent(referer);
cloudSource.source.getLoginUrl(`/dashboard/?action=${action}&ref=${referer}`)
.then((url) => window.open(url, "_system"))
cloudSource.source
.getLoginUrl(`/dashboard/?action=${action}&ref=${referer}`)
.then(url => window.open(url, "_system"))
.catch(() => window.open(`${this.settings.syncHostUrl}/dashboard/`, "_system"));
}
@ -187,7 +196,7 @@ padlock.SyncMixin = (superClass) => {
$l("Confirm"),
$l("Cancel"),
true,
(code) => {
code => {
if (code === null) {
// Dialog canceled
this.cancelConnect();
@ -196,12 +205,12 @@ padlock.SyncMixin = (superClass) => {
return Promise.reject("Please enter a valid login code!");
} else {
return this.activateToken(code)
.catch((e) => {
.catch(e => {
this._handleCloudError(e);
this.cancelConnect();
return e;
})
.then((success) => {
.then(success => {
// Error from previous catch clause; Cancel dialog
if (success.code) {
return null;
@ -218,8 +227,14 @@ padlock.SyncMixin = (superClass) => {
}
loginDialog() {
return this.prompt(this.loginInfoText(), $l("Enter Email Address"), "email", $l("Login"), false, false,
(val) => {
return this.prompt(
this.loginInfoText(),
$l("Enter Email Address"),
"email",
$l("Login"),
false,
false,
val => {
const input = document.createElement("input");
input.type = "email";
input.value = val;
@ -232,17 +247,17 @@ padlock.SyncMixin = (superClass) => {
}
}
)
.then((val) => val === null ? Promise.reject() : val)
.then(val => (val === null ? Promise.reject() : val))
.then(() => this.promptLoginCode())
.then((val) => val === null ? Promise.reject() : val)
.then(val => (val === null ? Promise.reject() : val))
.then(() => this.synchronize())
.catch((e) => e && this._handleCloudError(e));
.catch(e => e && this._handleCloudError(e));
}
loginInfoText() {
return $l(
"Log in now to unlock advanced features like automatic online backups and " +
"seamless synchronization between devices!"
"seamless synchronization between devices!"
);
}
@ -254,7 +269,8 @@ padlock.SyncMixin = (superClass) => {
cloudSource.password = this.password;
}
return this.collection.fetch(cloudSource)
return this.collection
.fetch(cloudSource)
.then(() => this.saveCollection())
.then(() => this.collection.save(cloudSource))
.then(() => {
@ -264,21 +280,17 @@ padlock.SyncMixin = (superClass) => {
if (cloudSource.password !== this.password) {
return this.choose(
$l("The master password you use locally does not match the one of your " +
"online account {0}. What do you want to do?", this.settings.syncEmail),
[
$l("Update Local Password"),
$l("Update Online Password"),
$l("Keep Both Passwords")
]
).then((choice) => {
$l(
"The master password you use locally does not match the one of your " +
"online account {0}. What do you want to do?",
this.settings.syncEmail
),
[$l("Update Local Password"), $l("Update Online Password"), $l("Keep Both Passwords")]
).then(choice => {
switch (choice) {
case 0:
this.setPassword(cloudSource.password).then(() => {
this.alert(
$l("Local password updated successfully."),
{ type: "success" }
);
this.alert($l("Local password updated successfully."), { type: "success" });
});
break;
case 1:
@ -288,7 +300,7 @@ padlock.SyncMixin = (superClass) => {
});
}
})
.catch((e) => {
.catch(e => {
this.dispatch("settings-changed");
if (this._handleCloudError(e)) {
this.dispatch("records-changed");
@ -321,12 +333,13 @@ padlock.SyncMixin = (superClass) => {
this.settings.syncToken = "";
this.dispatch("settings-changed");
return padlock.stats.get()
.then((stats) => {
return statsApi
.get()
.then(stats => {
const redirect = `/dashboard/?tid=${encodeURIComponent(stats.trackingID)}`;
return cloudSource.source.requestAuthToken(email, create, redirect, actType);
})
.then((authToken) => {
.then(authToken => {
// We're getting back the api key directly, but it will valid only
// after the user has visited the activation link in the email he was sent
this.settings.syncConnected = false;
@ -335,14 +348,18 @@ padlock.SyncMixin = (superClass) => {
this.settings.syncId = authToken.id;
this.dispatch("settings-changed");
})
.catch((e) => {
.catch(e => {
this.dispatch("settings-changed");
switch (typeof e === "string" ? e : e.code) {
case "account_not_found":
return this._requestAuthToken(email, true, actType);
case "rate_limit_exceeded":
this.alert($l("For security reasons only a limited amount of connection request " +
"are allowed at a time. Please wait a little before trying again!"));
this.alert(
$l(
"For security reasons only a limited amount of connection request " +
"are allowed at a time. Please wait a little before trying again!"
)
);
throw e;
default:
this._handleCloudError(e);
@ -372,14 +389,16 @@ padlock.SyncMixin = (superClass) => {
return false;
case "deprecated_api_version":
this.confirm(
$l("A newer version of Padlock is available now! Update now to keep using " +
"online features (you won't be able to sync with your account until then)!"),
$l(
"A newer version of Padlock is available now! Update now to keep using " +
"online features (you won't be able to sync with your account until then)!"
),
$l("Update Now"),
$l("Cancel"),
{ type: "info" }
).then((confirm) => {
).then(confirm => {
if (confirm) {
padlock.platform.checkForUpdates();
checkForUpdates();
}
});
return false;
@ -388,12 +407,15 @@ padlock.SyncMixin = (superClass) => {
return false;
case "invalid_container_data":
case "invalid_key_params":
this.alert($l(
"The data received from your online account seems to be corrupt and " +
"cannot be decrypted. This might be due to a network error but could " +
"also be the result of someone trying to compromise your connection to " +
"our servers. If the problem persists, please notify Padlock support!"
), { type: "warning" });
this.alert(
$l(
"The data received from your online account seems to be corrupt and " +
"cannot be decrypted. This might be due to a network error but could " +
"also be the result of someone trying to compromise your connection to " +
"our servers. If the problem persists, please notify Padlock support!"
),
{ type: "warning" }
);
return false;
case "decryption_failed":
case "encryption_failed":
@ -402,14 +424,20 @@ padlock.SyncMixin = (superClass) => {
// we need to prompt the user for the correct password.
this.prompt(
$l("Please enter the master password for the online account {0}.", this.settings.syncEmail),
$l("Enter Master Password"), "password", $l("Submit"), $l("Forgot Password"), true, (pwd) => {
$l("Enter Master Password"),
"password",
$l("Submit"),
$l("Forgot Password"),
true,
pwd => {
if (!pwd) {
return Promise.reject($l("Please enter a password!"));
}
cloudSource.password = pwd;
return this.collection.fetch(cloudSource)
return this.collection
.fetch(cloudSource)
.then(() => true)
.catch((e) => {
.catch(e => {
if (e.code == "decryption_failed") {
throw $l("Incorrect password. Please try again!");
} else {
@ -417,42 +445,49 @@ padlock.SyncMixin = (superClass) => {
return false;
}
});
})
.then((success) => {
if (success === null) {
this.forgotCloudPassword()
.then(() => this.synchronize());
}
if (success) {
this.synchronize();
}
});
}
).then(success => {
if (success === null) {
this.forgotCloudPassword().then(() => this.synchronize());
}
if (success) {
this.synchronize();
}
});
return false;
case "unsupported_container_version":
this.confirm($l(
"It seems the data stored on Padlock Cloud was saved with a newer version of Padlock " +
"and can not be opened with the version you are currently running. Please install the " +
"latest version of Padlock on this device!"
), $l("Check For Updates"), $l("Cancel")).
then((confirmed) => {
if (confirmed) {
padlock.platform.checkForUpdates();
}
});
this.confirm(
$l(
"It seems the data stored on Padlock Cloud was saved with a newer version of Padlock " +
"and can not be opened with the version you are currently running. Please " +
"install the latest version of Padlock on this device!"
),
$l("Check For Updates"),
$l("Cancel")
).then(confirmed => {
if (confirmed) {
checkForUpdates();
}
});
return false;
case "subscription_required":
return true;
case "failed_connection":
this.alert($l("Looks like we can't connect to our servers right now. Please check your internet " +
"connection and try again!"), { type: "warning", title: $l("Failed Connection") });
this.alert(
$l(
"Looks like we can't connect to our servers right now. Please check your internet " +
"connection and try again!"
),
{ type: "warning", title: $l("Failed Connection") }
);
return false;
default:
this.confirm(
e && e.message || $l("Something went wrong. Please try again later!"),
(e && e.message) || $l("Something went wrong. Please try again later!"),
$l("Contact Support"),
$l("Dismiss"),
{ type: "warning" }
).then((confirmed) => {
).then(confirmed => {
if (confirmed) {
window.open(`mailto:support@padlock.io?subject=Server+Error+(${e.code})`);
}
@ -466,46 +501,42 @@ padlock.SyncMixin = (superClass) => {
}
forgotCloudPassword() {
return this.promptForgotPassword()
.then((doReset) => {
if (doReset) {
return this.cloudSource.clear();
}
});
return this.promptForgotPassword().then(doReset => {
if (doReset) {
return this.cloudSource.clear();
}
});
}
_updateLastSync() {
return padlock.stats.get().then((s) => this.lastSync = formatDateFromNow(s.lastSync));
return statsApi.get().then(s => (this.lastSync = formatDateFromNow(s.lastSync)));
}
buySubscription(source) {
if (!this._plansPromise) {
this._plansPromise = this.cloudSource.source.getPlans();
}
this._plansPromise.then((plans) => this.openPaymentDialog(plans[0], source))
.then((success) => {
if (success) {
this.refreshAccount();
this.alert(
$l("Congratulations, your upgrade was successful! Enjoy using Padlock!"),
{ type: "success" }
);
}
});
this._plansPromise.then(plans => this.openPaymentDialog(plans[0], source)).then(success => {
if (success) {
this.refreshAccount();
this.alert($l("Congratulations, your upgrade was successful! Enjoy using Padlock!"), {
type: "success"
});
}
});
}
updatePaymentMethod(source) {
this.openPaymentDialog(null, source)
.then((success) => {
if (success) {
this.refreshAccount();
this.alert($l("Payment method updated successfully!"), { type: "success" });
}
});
this.openPaymentDialog(null, source).then(success => {
if (success) {
this.refreshAccount();
this.alert($l("Payment method updated successfully!"), { type: "success" });
}
});
}
openPaymentDialog(plan, source) {
return this.lineUpDialog("pl-payment-dialog", (dialog) => {
return this.lineUpDialog("pl-dialog-payment", dialog => {
dialog.plan = plan;
dialog.stripePubKey = this.settings.stripePubKey;
dialog.source = this.cloudSource.source;
@ -516,18 +547,20 @@ padlock.SyncMixin = (superClass) => {
}
reactivateSubscription() {
return this.cloudSource.source.subscribe()
return this.cloudSource.source
.subscribe()
.then(() => {
this.refreshAccount();
this.alert($l("Subscription reactivated successfully!"), { type: "success" });
})
.catch((e) => this._handleCloudError(e));
.catch(e => this._handleCloudError(e));
}
refreshAccount() {
if (this.settings.syncConnected) {
return this.cloudSource.source.getAccountInfo()
.catch((e) => this._handleCloudError(e))
return this.cloudSource.source
.getAccountInfo()
.catch(e => this._handleCloudError(e))
.then(() => this.dispatch("settings-changed"));
} else {
return Promise.resolve();
@ -536,35 +569,33 @@ padlock.SyncMixin = (superClass) => {
cancelSubscription() {
return this.confirm(
$l("Are you sure you want to cancel your subscription? You won't be able " +
"to continue using advanced features like automatic online backups and seamless " +
"synchronization between devices!"),
$l(
"Are you sure you want to cancel your subscription? You won't be able " +
"to continue using advanced features like automatic online backups and seamless " +
"synchronization between devices!"
),
$l("Cancel Subscription"),
$l("Don't Cancel"),
{ type: "warning" }
).then((confirmed) => {
).then(confirmed => {
if (confirmed) {
this.cloudSource.source.cancelSubscription()
.then(() => {
this.refreshAccount();
this.alert($l("Subscription canceled successfully."), { type: "success" });
});
this.cloudSource.source.cancelSubscription().then(() => {
this.refreshAccount();
this.alert($l("Subscription canceled successfully."), { type: "success" });
});
}
});
}
alertPromo() {
return this.lineUpDialog("pl-promo-dialog", (dialog) => {
return this.lineUpDialog("pl-dialog-promo", dialog => {
dialog.promo = this.promo;
return dialog.show();
})
.then((redeem) => {
if (redeem) {
this.buySubscription("App - Promo (Alert)");
}
});
}).then(redeem => {
if (redeem) {
this.buySubscription("App - Promo (Alert)");
}
});
}
};
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,114 +0,0 @@
import '../base/base.js';
import '../data/data.js';
const startedLoading = new Date().getTime();
const { init, track, setTrackingID } = padlock.tracking;
const { DataMixin } = padlock;
const { CloudSource } = padlock.source;
init(new CloudSource(DataMixin.settings), padlock.stats);
padlock.AnalyticsMixin = (superClass) => {
return class AnalyticsMixin extends superClass {
constructor() {
super();
this.listen("data-exported", () => padlock.stats.set({ lastExport: new Date().getTime() }));
this.listen("settings-changed", () => {
padlock.stats.set({
syncCustomHost: this.settings.syncCustomHost
});
});
this.listen("data-initialized", () => {
track("Setup", {
"With Email": !!this.settings.syncEmail
});
});
this.listen("data-loaded", () => track("Unlock"));
this.listen("data-unloaded", () => track("Lock"));
this.listen("data-reset", () => {
track("Reset Local Data")
.then(() => setTrackingID(""));
});
this.listen("sync-connect-start", (e) => {
padlock.stats.get().then((stats) => {
track("Start Pairing", {
"Source": stats.pairingSource,
"Email": e.detail.email
});
padlock.stats.set({ startedPairing: new Date().getTime() });
});
});
this.listen("sync-connect-success", () => {
return padlock.stats.get()
.then((stats) => {
track("Finish Pairing", {
"Success": true,
"Source": stats.pairingSource,
"$duration": (new Date().getTime() - stats.startedPairing) / 1000
});
});
});
this.listen("sync-connect-cancel", () => {
return padlock.stats.get()
.then((stats) => {
track("Finish Pairing", {
"Success": false,
"Canceled": true,
"Source": stats.pairingSource,
"$duration": (new Date().getTime() - stats.startedPairing) / 1000
});
});
});
this.listen("sync-disconnect", () => track("Unpair").then(() => setTrackingID("")));
let startedSync;
this.listen("sync-start", () => startedSync = new Date().getTime());
this.listen("sync-success", (e) => {
padlock.stats.set({ lastSync: new Date().getTime() })
.then(() => track("Synchronize", {
"Success": true,
"Auto Sync": e.detail ? e.detail.auto : false,
"$duration": (new Date().getTime() - startedSync) / 1000
}));
});
this.listen("sync-fail", (e) => track("Synchronize", {
"Success": false,
"Auto Sync": e.detail.auto,
"Error Code": e.detail.error.code,
"$duration": (new Date().getTime() - startedSync) / 1000
}));
}
connectedCallback() {
super.connectedCallback();
this.hasData()
.then((hasData) => track("Launch", {
"Clean Launch": !hasData,
"$duration": (new Date().getTime() - startedLoading) / 1000
}));
}
track(event, props) {
// Don't track events if user is using a custom padlock cloud instance
if (DataMixin.settings.syncCustomHost) {
return Promise.resolve();
}
track(event, props);
}
};
};

View File

@ -1,268 +0,0 @@
import '../base/base.js';
import '../locale/locale.js';
const { wait } = padlock.util;
const { isChromeApp, isChromeOS, getReviewLink } = padlock.platform;
const day = 1000 * 60 * 60 * 24;
let stats;
const statsLoaded = padlock.stats.get().then((s) => stats = s);
function daysPassed(date) {
return (new Date().getTime() - date) / day;
}
padlock.HintsMixin = (superClass) => {
return class HintsMixin extends superClass {
constructor() {
super();
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindBackup()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._remindSync()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._askFeedback()));
this.listen("data-loaded", () => statsLoaded.then(() => wait(1000)).then(() => this._crossPlatformHint()));
this.listen("auto-lock", () => statsLoaded.then(() => this._showAutoLockNotice()));
this.listen("settings-changed", () => this._notifySubStatus());
this.listen("sync-connect-success", () => this._notifySubStatus());
this.listen("data-loaded", () => wait(1000).then(() => this._notifySubStatus()));
}
ready() {
super.ready();
statsLoaded
.then(() => wait(1000))
.then(() => this._notifyChromeAppDeprecation());
}
_remindBackup() {
if (
daysPassed(stats.lastExport || stats.firstLaunch) > 7 &&
daysPassed(stats.lastBackupReminder || stats.firstLaunch) > 7 &&
!this.settings.syncConnected
) {
this.choose($l(
"Have you backed up your data yet? Remember that by default your data is only stored " +
"locally and can not be recovered in case your device gets damaged or lost. " +
"Log in now to enable automatic online backups!"
), [$l("Log In"), $l("Do Nothing")], { type: "warning" })
.then((choice) => {
switch (choice) {
case 0:
padlock.stats.set({ pairingSource: "Backup Reminder" });
this._openCloudView();
break;
}
});
padlock.stats.set({ lastBackupReminder: new Date().getTime() });
}
}
_remindSync() {
const daysSinceLastSync = daysPassed(stats.lastSync || stats.firstLaunch);
if (
this.settings.syncConnected &&
daysSinceLastSync > 7 &&
daysPassed(stats.lastSyncReminder || stats.firstLaunch) > 7
) {
this.choose($l(
"The last time you synced your data with your account was {0} days ago! You should " +
"synchronize regularly to keep all your devices up-to-date and to make sure you always " +
"have a recent backup in the cloud.",
Math.floor(daysSinceLastSync)
), [$l("Synchronize Now"), $l("Turn On Auto Sync"), $l("Do Nothing")], { type: "warning" })
.then((choice) => {
switch (choice) {
case 0:
this.synchronize();
break;
case 1:
this.settings.syncAuto = true;
this.dispatch("settings-changed");
this.synchronize();
break;
}
});
padlock.stats.set({ lastSyncReminder: new Date().getTime() });
}
}
_notifyChromeAppDeprecation() {
isChromeOS().then((yes) => {
if (isChromeApp() && !yes) {
this.confirm($l(
"In August 2016, Google announced that they will be discontinuing Chrome Apps for all " +
"operating systems other than ChromeOS. This means that by early 2018, you will no " +
"longer be able to load this app! Don't worry though, because Padlock is now available " +
"as a native app for Windows, MacOS and Linux! Head over to our website to find out " +
"how to switch now!"
), $l("Learn More"), $l("Dismiss"), { type: "info", hideIcon: true })
.then((confirmed) => {
if (confirmed) {
window.open("https://padlock.io/news/discontinuing-chrome/", "_system");
}
});
}
});
}
_askFeedback() {
if (
stats.launchCount > 20 &&
!stats.dontAskFeeback &&
!stats.lastAskedFeedback
) {
this.choose($l(
"Hey there! Sorry to bother you, but we'd love to know how you are liking Padlock so far!"
), [
$l("I Love it!") + " \u2665",
$l("It's not bad, but..."),
$l("Hate it.")
])
.then((rating) => {
this.track("Rate App", { "Rating": rating });
padlock.stats.set({
lastAskedFeedback: new Date().getTime(),
lastRating: rating,
lastRatedVersion: this.settings.version
});
switch (rating) {
case 0:
this.choose($l(
"So glad to hear you like our app! Would you mind taking a second to " +
"let others know what you think about Padlock?"
), [
$l("Rate Padlock"),
$l("No Thanks")
])
.then((choice) => {
switch (choice) {
case 0:
this.track("Review App", { "Rating": rating });
this._sendFeedback(rating);
padlock.stats.set({ lastReviewed: new Date().getTime() });
break;
case 1:
padlock.stats.set({ dontAskFeedback: true });
break;
}
});
break;
case 1:
case 2:
this.choose($l(
"Your opinion as a user is very important to us and we're always " +
"looking for feedback and suggestions on how to improve Padlock. " +
"Would you mind taking a second to let us know what you think about " +
"the app?"
), [
$l("Send Feedback"),
$l("No Thanks")
])
.then((choice) => {
if (choice === 0) {
this.track("Provide Feedback", { "Rating": rating });
this._sendFeedback(rating);
}
});
break;
}
});
}
}
_sendFeedback(rating) {
getReviewLink(rating).then((link) => window.open(link, "_system"));
}
_showAutoLockNotice() {
if (!stats.hasShownAutoLockNotice) {
const minutes = this.settings.autoLockDelay;
setTimeout(() => {
this.alert($l("Padlock was automatically locked after {0} {1} " +
"of inactivity. You can change this behavior from the settings page.",
minutes, minutes > 1 ? $l("minutes") : $l("minute")));
}, 1000);
padlock.stats.set({ hasShownAutoLockNotice: true });
}
}
_notifySubStatus() {
if (this.promo && !this._hasShownPromo) {
this.alertPromo();
this._hasShownPromo = true;
return;
}
if (
this.settings.syncSubStatus === "trialing" &&
(!stats.lastTrialEndsReminder || daysPassed(stats.lastTrialEndsReminder) > 7)
) {
this.buySubscription("App - Trialing (Alert)");
padlock.stats.set({ lastTrialEndsReminder: new Date().getTime() });
return;
}
if (this.settings.syncConnected && this.settings.syncSubStatus !== this._lastSubStatus) {
this._lastSubStatus = this.settings.syncSubStatus;
if (this.isTrialExpired()) {
this.alert(this.trialExpiredMessage(), {
type: "warning",
title: $l("Trial Expired"),
options: [$l("Upgrade Now")]
}).then((choice) => {
if (choice === 0) {
this.buySubscription("App - Trial Expired (Alert)");
}
});
} else if (this.isSubUnpaid()) {
this.alert(this.subUnpaidMessage(), {
type: "warning",
title: $l("Payment Failed"),
options: [$l("Update Payment Method"), $l("Contact Support")]
}).then((choice) => {
if (choice === 0) {
this.updatePaymentMethod("App - Payment Failed (Alert)");
} else if (choice === 1) {
window.open("mailto:support@padlock.io", "_system");
}
});
}
}
}
_crossPlatformHint() {
if (
!this.settings.syncConnected &&
!stats.hasShownCrossPlatformHint
) {
this.confirm(
$l(
"Padlock lets you share your data seamlessly across all your devices like phones, " +
"tablets and computers! Log in now and we'll get you set up in just few steps!"
),
$l("Log In"),
$l("No Thanks"),
{ title: $l("Did You Know?"), type: "question" }
)
.then((confirmed) => {
if (confirmed) {
padlock.stats.set({ pairingSource: "Cross-Platform Hint" });
this._openCloudView();
}
});
padlock.stats.set({ hasShownCrossPlatformHint: new Date().getTime() });
}
}
};
};

View File

@ -1,44 +0,0 @@
import '../base/base.js';
import '../locale/locale.js';
const { Messages } = padlock.messages;
const { FileSource } = padlock.source;
padlock.MessagesMixin = (superClass) => {
return class MessagesMixin extends superClass {
ready() {
super.ready();
this._messages = new Messages(
"https://padlock.io/messages.json",
new FileSource("read-messages.json"),
this.settings
);
this.listen("data-loaded", () => this.checkMessages());
}
checkMessages() {
padlock.util.wait(1000)
.then(() => this._messages.fetch())
.then((aa) => aa.forEach((a) => this._displayMessage(a)));
}
_displayMessage(a) {
if (a.link) {
this.confirm(a.text, $l("Learn More"), $l("Dismiss"), { type: "info" })
.then((confirmed) => {
if (confirmed) {
window.open(a.link, "_system");
}
this._messages.markRead(a);
});
} else {
this.alert(a.text)
.then(() => this._messages.markRead(a));
}
}
};
};

View File

@ -1,136 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../locale/locale.js';
const { setClipboard } = padlock.platform;
const { LocaleMixin } = padlock;
class Clipboard extends LocaleMixin(padlock.BaseElement) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: flex;
text-align: center;
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
position: fixed;
left: 15px;
right: 15px;
bottom: 15px;
z-index: 10;
max-width: 400px;
margin: 0 auto;
border-radius: var(--border-radius);
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
color: var(--color-background);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
}
:host(:not(.showing)) {
transform: translateY(130%);
}
.content {
flex: 1;
padding: 15px;
}
.name {
font-weight: bold;
}
button {
height: auto;
line-height: normal;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.countdown {
font-size: var(--font-size-small);
}
</style>
<div class="content">
<div class="title">[[ \$l("Copied To Clipboard:") ]]</div>
<div class="name">[[ record.name ]] / [[ field.name ]]</div>
</div>
<button class="tiles-2 tap" on-click="clear">
<div><strong>[[ \$l("Clear") ]]</strong></div>
<div class="countdown">[[ _tMinusClear ]]s<div>
</div></div></button>
`;
}
static get is() { return "pl-clipboard"; }
static get properties() { return {
record: Object,
field: Object
}; }
set(record, field, duration = 60) {
clearInterval(this._interval);
this.record = record;
this.field = field;
setClipboard(field.value);
this.classList.add("showing");
const tStart = Date.now();
this._tMinusClear = duration;
this._interval = setInterval(() => {
const dt = tStart + duration * 1000 - Date.now();
if (dt <= 0) {
this.clear();
} else {
this._tMinusClear = Math.floor(dt/1000);
}
}, 1000);
return new Promise((resolve) => {
this._resolve = resolve;
});
}
clear() {
clearInterval(this._interval);
setClipboard(" ");
this.classList.remove("showing");
typeof this._resolve === "function" && this._resolve();
this._resolve = null;
}
}
window.customElements.define(Clipboard.is, Clipboard);
let clipboardSingleton;
padlock.ClipboardMixin = (baseClass) => {
return class ClipboardMixin extends baseClass {
setClipboard(record, field, duration) {
if (!clipboardSingleton) {
clipboardSingleton = document.createElement("pl-clipboard");
document.body.appendChild(clipboardSingleton);
clipboardSingleton.offsetLeft;
}
return clipboardSingleton.set(record, field, duration);
}
clearClipboard() {
if (clipboardSingleton) {
clipboardSingleton.clear();
}
}
};
};

View File

@ -1,124 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../locale/locale.js';
import './dialog.js';
const defaultButtonLabel = $l("OK");
class DialogAlert extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
};
}
:host([type="warning"]) {
--pl-dialog-inner: {
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
};
}
:host([type="plain"]) {
--pl-dialog-inner: {
background: var(--color-background);
};
}
:host([hide-icon]) .info-icon {
display: none;
}
:host([hide-icon]) .info-text,
:host([hide-icon]) .info-title {
text-align: center;
}
.info-text:not(.small) {
font-size: var(--font-size-default);
}
</style>
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
<div class="info" hidden\$="[[ _hideInfo(title, message) ]]">
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
<div class="info-body">
<div class="info-title">[[ title ]]</div>
<div class\$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
</div>
</div>
<template is="dom-repeat" items="[[ options ]]">
<button on-click="_selectOption" class\$="[[ _buttonClass(index) ]]">[[ item ]]</button>
</template>
</pl-dialog>
`;
}
static get is() { return "pl-dialog-alert"; }
static get properties() { return {
buttonLabel: { type: String, value: defaultButtonLabel },
title: { type: String, value: ""},
message: { type: String, value: "" },
options: { type: Array, value: ["OK"] },
preventDismiss: { type: Boolean, value: false },
type: { type: String, value: "info", reflectToAttribute: true },
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
open: { type: Boolean, value: false }
}; }
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
this.message = message;
this.title = title;
this.type = type;
this.preventDismiss = preventDismiss;
this.options = options;
this.hideIcon = hideIcon;
setTimeout(() => this.open = true, 10);
return new Promise((resolve) => {
this._resolve = resolve;
});
}
_icon() {
switch (this.type) {
case "info":
return "info-round";
case "warning":
return "error";
case "success":
return "success";
case "question":
return "question";
}
}
_selectOption(e) {
this.open = false;
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
this._resolve = null;
}
_dialogDismiss() {
typeof this._resolve === "function" && this._resolve();
this._resolve = null;
}
_textClass() {
return this.title ? "small" : "";
}
_buttonClass(index) {
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
}
_hideInfo() {
return !this.title && !this.message;
}
}
window.customElements.define(DialogAlert.is, DialogAlert);

View File

@ -1,58 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../locale/locale.js';
import './dialog.js';
const defaultMessage = $l("Are you sure you want to do this?");
const defaultConfirmLabel = $l("Confirm");
const defaultCancelLabel = $l("Cancel");
class DialogConfirm extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared"></style>
<pl-dialog open="{{ open }}" prevent-dismiss="">
<div class="message tiles-1">{{ message }}</div>
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
</pl-dialog>
`;
}
static get is() { return "pl-dialog-confirm"; }
static get properties() { return {
confirmLabel: { type: String, value: defaultConfirmLabel },
cancelLabel: { type: String, value: defaultCancelLabel },
message: { type: String, value: defaultMessage },
open: { type: Boolean, value: false }
}; }
_confirm() {
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
this.open = false;
typeof (this._resolve === "function") && this._resolve(true);
this._resolve = null;
}
_cancel() {
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
this.open = false;
typeof (this.resolve === "function") && this._resolve(false);
this._resolve = null;
}
confirm(message, confirmLabel, cancelLabel) {
this.message = message || defaultMessage;
this.confirmLabel = confirmLabel || defaultConfirmLabel;
this.cancelLabel = cancelLabel || defaultCancelLabel;
this.open = true;
return new Promise((resolve) => {
this._resolve = resolve;
});
}
}
window.customElements.define(DialogConfirm.is, DialogConfirm);

View File

@ -1,62 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../locale/locale.js';
import './dialog.js';
class DialogOptions extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared"></style>
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp="">
<div class="message tiles-1">[[ message ]]</div>
</template>
<template is="dom-repeat" items="[[ options ]]">
<button class\$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
</template>
</pl-dialog>
`;
}
static get is() { return "pl-dialog-options"; }
static get properties() { return {
message: { type: String, value: "" },
open: { type: Boolean, value: false },
options: { type: Array, value: [$l("Dismiss")] },
preventDismiss: { type: Boolean, value: false }
}; }
choose(message, options) {
this.message = message || "";
this.options = options || this.options;
setTimeout(() => this.open = true, 50);
return new Promise((resolve) => {
this._resolve = resolve;
});
}
_selectOption(e) {
this.open = false;
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
this._resolve = null;
}
_buttonClass(index) {
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
}
_hasMessage(message) {
return !!message;
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(-1);
this._resolve = null;
}
}
window.customElements.define(DialogOptions.is, DialogOptions);

View File

@ -1,111 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../input/input.js';
import '../loading-button/loading-button.js';
import '../locale/locale.js';
import './dialog.js';
const defaultConfirmLabel = $l("OK");
const defaultCancelLabel = $l("Cancel");
const defaultType = "text";
const defaultPlaceholder = "";
class DialogPrompt extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
};
}
pl-input {
text-align: center;
}
.validation-message {
position: relative;
margin-top: 15px;
font-weight: bold;
font-size: var(--font-size-small);
color: var(--color-error);
text-shadow: none;
text-align: center;
}
</style>
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
<div class="message tiles-1" hidden\$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
<button class="tap tiles-4" on-click="_dismiss" hidden\$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
</pl-dialog>
`;
}
static get is() { return "pl-dialog-prompt"; }
static get properties() { return {
confirmLabel: { type: String, value: defaultConfirmLabel },
cancelLabel: { type: String, value: defaultCancelLabel },
message: { type: String, value: "" },
open: { type: Boolean, value: false },
placeholder: { type: String, value: ""},
preventDismiss: { type: Boolean, value: true},
type: { type: String, value: defaultType },
validationFn: Function,
_validationMessage: { type: String, value: "" }
}; }
_confirm() {
this.$.confirmButton.start();
const val = this.$.input.value;
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
p.then((v) => {
this._validationMessage = "";
this.$.confirmButton.success();
typeof this._resolve === "function" && this._resolve(v);
this._resolve = null;
this.open = false;
}).catch((e) => {
this.$.dialog.rumble();
this._validationMessage = e;
this.$.confirmButton.fail();
});
}
_dismiss() {
typeof this._resolve === "function" && this._resolve(null);
this._resolve = null;
this.open = false;
}
_hasMessage() {
return !!this.message;
}
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
this.$.confirmButton.stop();
this.message = message || "";
this.type = type || defaultType;
this.placeholder = placeholder || defaultPlaceholder;
this.confirmLabel = confirmLabel || defaultConfirmLabel;
this.cancelLabel = cancelLabel || defaultCancelLabel;
this._hideCancelButton = cancelLabel === false;
this.preventDismiss = preventDismiss;
this.validationFn = validation;
this._validationMessage = "";
this.$.input.value = "";
this.open = true;
setTimeout(() => this.$.input.focus(), 100);
return new Promise((resolve) => {
this._resolve = resolve;
});
}
}
window.customElements.define(DialogPrompt.is, DialogPrompt);

View File

@ -1,181 +0,0 @@
import '../../styles/shared.js';
import '../animation/animation.js';
import '../base/base.js';
import '../input/input.js';
class Dialog extends padlock.AnimationMixin(padlock.BaseElement) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: block;
@apply --fullbleed;
position: fixed;
z-index: 10;
@apply --scroll;
}
:host(:not(.open)) {
pointer-events: none;
}
.outer {
min-height: 100%;
display: flex;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
box-sizing: border-box;
}
.scrim {
display: block;
background: var(--color-background);
opacity: 0;
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
transform: translate3d(0, 0, 0);
@apply --fullbleed;
position: fixed;
}
:host(.open) .scrim {
opacity: 0.90;
}
.inner {
width: 100%;
box-sizing: border-box;
max-width: var(--pl-dialog-max-width, 400px);
z-index: 1;
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
background: var(--color-background);
color: var(--color-foreground);
border-radius: var(--border-radius);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
overflow: hidden;
@apply --pl-dialog-inner;
}
.outer {
transform: translate3d(0, 0, 0);
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
}
:host(:not(.open)) .outer {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.8);
}
</style>
<div class="scrim"></div>
<div class="outer" on-click="dismiss">
<slot name="before"></slot>
<div id="inner" class="inner" on-click="_preventDismiss">
<slot></slot>
</div>
<slot name="after"></slot>
</div>
`;
}
static get is() { return "pl-dialog"; }
static get properties() { return {
animationOptions: {
type: Object,
value: {
duration: 500,
fullDuration: 700
}
},
open: {
type: Boolean,
value: false,
notify: true,
observer: "_openChanged"
},
isShowing: {
type: Boolean,
value: false,
notify: true
},
preventDismiss: {
type: Boolean,
value: false
}
}; }
ready() {
super.ready();
// window.addEventListener("keydown", (e) => {
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
// this.dismiss();
// // e.preventDefault();
// // e.stopPropagation();
// }
// });
window.addEventListener("backbutton", (e) => {
if (this.open) {
this.dismiss();
e.preventDefault();
e.stopPropagation();
}
});
}
rumble() {
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
}
//* Changed handler for the _open_ property. Shows/hides the dialog
_openChanged() {
clearTimeout(this._hideTimeout);
// Set _display: block_ if we're showing. If we're hiding
// we need to wait until the transitions have finished before we
// set _display: none_.
if (this.open) {
if (padlock.Input.activeInput) {
padlock.Input.activeInput.blur();
}
this.style.display = "";
this.isShowing = true;
} else {
this._hideTimeout = window.setTimeout(() => {
this.style.display = "none";
this.isShowing = false;
}, 400);
}
this.offsetLeft;
this.classList.toggle("open", this.open);
this.dispatchEvent(new CustomEvent(
this.open ? "dialog-open" : "dialog-close",
{ bubbles: true, composed: true }
));
}
_preventDismiss(e) {
e.stopPropagation();
}
dismiss() {
if (!this.preventDismiss) {
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
this.open = false;
}
}
}
window.customElements.define(Dialog.is, Dialog);

View File

@ -1,159 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../icon/icon.js';
import '../locale/locale.js';
const exportCSVWarning = $l(
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
"a secure, encrypted file, instead! Are you sure you want to proceed?"
);
const { LocaleMixin, DialogMixin, DataMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const { isCordova, setClipboard } = padlock.platform;
const { toPadlock, toCSV } = padlock.exp;
class PlExport extends applyMixins(
BaseElement,
DataMixin,
LocaleMixin,
DialogMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: block;
}
.row {
display: flex;
align-items: center;
}
.label {
padding: 0 15px;
flex: 1;
}
pl-icon {
width: 50px;
height: 50px;
}
</style>
<div class="tiles tiles-1 row">
<div class="label">[[ \$l("As CSV") ]]</div>
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden\$="[[ _isMobile() ]]"></pl-icon>
</div>
<div class="tiles tiles-2 row">
<div class="label">[[ \$l("As Encrypted File") ]]</div>
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden\$="[[ _isMobile() ]]"></pl-icon>
</div>
`;
}
static get is() { return "pl-export"; }
static get properties() { return {
exportRecords: Array
}; }
_downloadCSV() {
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" })
.then((confirm) => {
if (confirm) {
setTimeout(() => {
const date = new Date().toISOString().substr(0, 10);
const fileName = `padlock-export-${date}.csv`;
const csv = toCSV(this.exportRecords);
const a = document.createElement("a");
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
a.download = fileName;
a.click();
this.dispatch("data-exported");
}, 500);
}
});
}
_copyCSV() {
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" })
.then((confirm) => {
if (confirm) {
setClipboard(toCSV(this.exportRecords))
.then(() => this.alert($l("Your data has successfully been copied to the system " +
"clipboard. You can now paste it into the spreadsheet program of your choice.")));
this.dispatch("data-exported");
}
});
}
_getEncryptedData() {
return this.prompt($l("Please choose a password to protect your data. This may be the same as " +
"your master password or something else, but make sure it is sufficiently strong!"),
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"))
.then((pwd) => {
if (!pwd) {
if (pwd === "") {
this.alert($l("Please enter a password!"));
}
return Promise.reject();
}
if (zxcvbn(pwd).score < 2) {
return this.confirm($l(
"WARNING: The password you entered is weak which makes it easier for " +
"attackers to break the encryption used to protect your data. Try to use a longer " +
"password or include a variation of uppercase, lowercase and special characters as " +
"well as numbers."
), $l("Use Anyway"), $l("Choose Different Password"), { type: "warning" }).then((confirm) => {
if (!confirm) {
return Promise.reject();
}
return toPadlock(this.exportRecords, pwd);
});
} else {
return toPadlock(this.exportRecords, pwd);
}
});
}
_downloadEncrypted() {
this._getEncryptedData()
.then((data) => {
const a = document.createElement("a");
const date = new Date().toISOString().substr(0, 10);
const fileName = `padlock-export-${date}.pls`;
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
a.download = fileName;
setTimeout(() => {
a.click();
this.dispatch("data-exported");
}, 500);
});
}
_copyEncrypted() {
this._getEncryptedData()
.then((data) => {
setClipboard(data)
.then(() => {
this.alert($l("Your data has successfully been copied to the system clipboard."),
{ type: "success" });
});
this.dispatch("data-exported");
});
}
_isMobile() {
return isCordova();
}
}
window.customElements.define(PlExport.is, PlExport);

View File

@ -1,254 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import autosize from '../../../../../node_modules/autosize/src/autosize.js';
let activeInput = null;
// On touch devices, blur active input when tapping on a non-input
document.addEventListener("touchend", () => {
if (activeInput) {
activeInput.blur();
}
});
class PlInput extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: block;
position: relative;
}
:host(:not([multiline])) {
padding: 0 10px;
height: var(--row-height);
}
input {
box-sizing: border-box;
text-overflow: ellipsis;
}
input, textarea {
text-align: inherit;
width: 100%;
height: 100%;
min-height: inherit;
line-height: inherit;
}
::-webkit-search-cancel-button {
display: none;
}
::-webkit-input-placeholder {
text-shadow: inherit;
color: inherit;
opacity: 0.5;
@apply --pl-input-placeholder;
}
.mask {
@apply --fullbleed;
pointer-events: none;
font-size: 150%;
line-height: 22px;
letter-spacing: -4.5px;
margin-left: -4px;
}
input[disabled], textarea[disabled] {
opacity: 1;
-webkit-text-fill-color: currentColor;
}
input[invisible], textarea[invisible] {
opacity: 0;
}
</style>
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
<textarea id="input" value="{{ value::input }}" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" rows="1" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" tabindex\$="[[ _tabIndex(noTab) ]]" invisible\$="[[ _showMask(masked, value, focused) ]]" disabled\$="[[ disabled ]]"></textarea>
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled=""></textarea>
</template>
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
<input id="input" value="{{ value::input }}" tabindex\$="[[ _tabIndex(noTab) ]]" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" type\$="[[ type ]]" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" required\$="[[ required ]]" pattern\$="[[ pattern ]]" disabled\$="[[ disabled ]]" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" invisible\$="[[ _showMask(masked, value, focused) ]]">
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled="">
</template>
`;
}
static get is() { return "pl-input"; }
static get activeInput() { return activeInput; }
static get properties() { return {
autosize: {
type: Boolean,
value: false
},
autocapitalize: {
type: Boolean,
value: false
},
disabled: {
type: Boolean,
value: false
},
focused: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
readonly: true
},
invalid: {
type: Boolean,
value: false,
notifiy: true,
reflectToAttribute: true,
readonly: true
},
masked: {
type: Boolean,
value: false,
reflectToAttribute: true
},
multiline: {
type: Boolean,
value: false,
reflectToAttribute: true
},
pattern: {
type: String,
value: null
},
placeholder: {
type: String,
value: ""
},
noTab: {
type: Boolean,
value: false
},
readonly: {
type: Boolean,
value: false
},
required: {
type: Boolean,
value: ""
},
type: {
type: String,
value: "text"
},
selectOnFocus: {
type: Boolean,
value: false
},
value: {
type: String,
value: "",
notify: true,
observer: "_valueChanged"
}
}; }
get inputElement() {
return this.root.querySelector(this.multiline ? "textarea" : "input");
}
_domChange() {
if (this.autosize && this.multiline && this.inputElement) {
autosize(this.inputElement);
}
setTimeout(() => this._valueChanged(), 50);
}
_stopPropagation(e) {
e.stopPropagation();
}
_focused(e) {
e.stopPropagation();
this.focused = true;
activeInput = this;
this.dispatchEvent(new CustomEvent("focus"));
if (this.selectOnFocus) {
setTimeout(() => this.selectAll(), 10);
}
}
_blurred(e) {
e.stopPropagation();
this.focused = false;
if (activeInput === this) {
activeInput = null;
}
this.dispatchEvent(new CustomEvent("blur"));
}
_changeHandler(e) {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("change"));
}
_keydown(e) {
if (e.key === "Enter" && !this.multiline) {
this.dispatchEvent(new CustomEvent("enter"));
e.preventDefault();
e.stopPropagation();
} else if (e.key === "Escape") {
this.dispatchEvent(new CustomEvent("escape"));
e.preventDefault();
e.stopPropagation();
}
}
_valueChanged() {
this.invalid = this.inputElement && !this.inputElement.checkValidity();
if (this.autosize && this.multiline) {
autosize.update(this.inputElement);
}
}
_tabIndex(noTab) {
return noTab ? "-1" : "";
}
_showMask() {
return this.masked && !!this.value && !this.focused;
}
_mask(value) {
return value && value.replace(/[^\n]/g, "\u2022");
}
_computeAutoCapitalize() {
return this.autocapitalize ? "" : "off";
}
focus() {
this.inputElement.focus();
}
blur() {
this.inputElement.blur();
}
selectAll() {
try {
this.inputElement.setSelectionRange(0, this.value.length);
} catch (e) {
this.inputElement.select();
}
}
}
window.customElements.define(PlInput.is, PlInput);
padlock.Input = PlInput;

File diff suppressed because one or more lines are too long

View File

@ -1,116 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
class Notification extends padlock.BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
display: block;
text-align: center;
font-weight: bold;
transition: transform 0.5s cubic-bezier(1, -0.3, 0, 1.3);
position: fixed;
left: 15px;
right: 15px;
bottom: 15px;
z-index: 10;
max-width: 400px;
margin: 0 auto;
color: var(--color-background);
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
}
:host(:not(.showing)) {
transform: translateY(130%);
}
.text {
padding: 15px;
position: relative;
}
.background {
opacity: 0.95;
border-radius: var(--border-radius);
@apply --fullbleed;
background: linear-gradient(90deg, rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
}
:host(.error) .background, :host(.warning) .background {
background: linear-gradient(90deg, #f49300 0%, #f25b00 100%);
}
</style>
<div class="background"></div>
<div class="text" on-click="_click">{{ message }}</div>
`;
}
static get is() { return "pl-notification"; }
static get properties() { return {
message: String,
type: {
type: String,
value: "info",
observer: "_typeChanged"
}
}; }
show(message, type, duration) {
if (message) {
this.message = message;
}
if (type) {
this.type = type;
}
this.classList.add("showing");
if (duration) {
setTimeout(() => this.hide(false), duration);
}
return new Promise((resolve) => {
this._resolve = resolve;
});
}
hide(clicked) {
this.classList.remove("showing");
typeof this._resolve === "function" && this._resolve(clicked);
this._resolve = null;
}
_typeChanged(newType, oldType) {
this.classList.remove(oldType);
this.classList.add(newType);
}
_click() {
this.hide(true);
}
}
window.customElements.define(Notification.is, Notification);
let notificationSingleton;
padlock.NotificationMixin = (baseClass) => {
return class NotificationMixin extends baseClass {
notify(message, type, duration) {
if (!notificationSingleton) {
notificationSingleton = document.createElement("pl-notification");
document.body.appendChild(notificationSingleton);
notificationSingleton.offsetLeft;
}
return notificationSingleton.show(message, type, duration);
}
};
};

View File

@ -1,52 +0,0 @@
import '../../styles/shared.js';
import '../base/base.js';
import '../dialog/dialog.js';
import './promo.js';
const { BaseElement } = padlock;
class PlExportDialog extends BaseElement {
static get template() {
return Polymer.html`
<style include="shared">
:host {
--pl-dialog-inner: {
background: linear-gradient(180deg, #555 0%, #222 100%);
};
}
</style>
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
</pl-dialog>
`;
}
static get is() { return "pl-promo-dialog"; }
static get properties() { return {
promo: Object
}; }
_dismiss() {
this.$.dialog.open = false;
typeof this._resolve === "function" && this._resolve(false);
this._resolve = null;
}
_redeem() {
this.$.dialog.open = false;
typeof this._resolve === "function" && this._resolve(true);
this._resolve = null;
}
show() {
setTimeout(() => this.$.dialog.open = true, 10);
return new Promise((resolve) => {
this._resolve = resolve;
});
}
}
window.customElements.define(PlExportDialog.is, PlExportDialog);

View File

@ -1,523 +0,0 @@
import '../../styles/shared.js';
import '../animation/animation.js';
import '../base/base.js';
import '../data/data.js';
import '../dialog/dialog-mixin.js';
import '../export/export.js';
import '../icon/icon.js';
import '../locale/locale.js';
import '../slider/slider.js';
import '../sync/sync.js';
import '../toggle/toggle-button.js';
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin, BaseElement } = padlock;
const { applyMixins } = padlock.util;
const { isCordova, getReviewLink, isTouch, getDesktopSettings, checkForUpdates,
saveDBAs, loadDB, isElectron } = padlock.platform;
class SettingsView extends applyMixins(
BaseElement,
DataMixin,
LocaleMixin,
DialogMixin,
AnimationMixin,
SyncMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
@keyframes beat {
0% { transform: scale(1); }
5% { transform: scale(1.4); }
15% { transform: scale(1); }
}
:host {
@apply --fullbleed;
display: flex;
flex-direction: column;
}
main {
background: var(--color-quaternary);
}
section {
transform: translate3d(0, 0, 0);
}
section .info {
display: block;
font-size: var(--font-size-small);
text-align: center;
line-height: normal;
padding: 15px;
height: auto;
}
button {
width: 100%;
box-sizing: border-box;
}
button > pl-icon {
position: absolute;
}
pl-toggle-button {
display: block;
}
input[type="file"] {
display: none;
}
label {
line-height: 0;
}
.padlock-heart {
display: inline-block;
margin: 0 5px;
animation: beat 5s infinite;
}
.padlock-heart::before {
font-family: "FontAwesome";
content: "\\f004";
}
.made-in {
font-size: var(--font-size-tiny);
margin-top: 3px;
}
.db-path {
font-weight: bold;
margin-top: 5px;
word-wrap: break-word;
font-size: var(--font-size-tiny);
}
#customUrlInput:not([invalid]) + .url-warning {
display: none;
}
.url-warning {
font-size: var(--font-size-small);
text-align: center;
padding: 15px;
border-top: solid 1px var(--border-color);
color: var(--color-error);
}
.feature-locked {
font-size: var(--font-size-tiny);
color: var(--color-error);
margin: -14px 15px 12px 15px;
}
</style>
<header>
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
<div class="title">[[ \$l("Settings") ]]</div>
<pl-icon></pl-icon>
</header>
<main>
<section>
<div class="section-header">[[ \$l("Auto Lock") ]]</div>
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ \$l('Lock Automatically') ]]" class="tap" reverse="" on-change="settingChanged"></pl-toggle-button>
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ \$l(' min') ]]" label="[[ \$l('After') ]]" hidden\$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
</section>
<section>
<div class="section-header">[[ \$l("Synchronization") ]]</div>
<div disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ \$l('Sync Automatically') ]]" reverse="" class="tap" on-change="settingChanged"></pl-toggle-button>
<div class="feature-locked" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log in to enable auto sync!") ]]</div>
<div class="feature-locked" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
<div class="feature-locked" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
</div>
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ \$l('Use Custom Server') ]]" reverse="" on-change="_customHostChanged" class="tap" disabled\$="[[ settings.syncConnected ]]"></pl-toggle-button>
<div class="tap" hidden\$="[[ !settings.syncCustomHost ]]" disabled\$="[[ settings.syncConnected ]]">
<pl-input id="customUrlInput" placeholder="[[ \$l('Enter Custom URL') ]]" value="{{ settings.syncHostUrl }}" pattern="^https://[^\\s/\$.?#].[^\\s]*\$" required="" on-change="settingChanged"></pl-input>
<div class="url-warning">
<strong>[[ \$l("Invalid URL") ]]</strong> -
[[ \$l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
</div>
</div>
</section>
<section>
<button on-click="_changePassword" class="tap">[[ \$l("Change Master Password") ]]</button>
<button on-click="_resetData" class="tap">[[ \$l("Reset App") ]]</button>
<button class="tap" on-click="_import">[[ \$l("Import...") ]]</button>
<button class="tap" on-click="_export">[[ \$l("Export...") ]]
</button></section>
<section hidden\$="[[ !_isDesktop() ]]">
<div class="section-header">[[ \$l("Updates") ]]</div>
<pl-toggle-button id="autoUpdatesButton" label="[[ \$l('Automatically Install Updates') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
<pl-toggle-button id="betaReleasesButton" label="[[ \$l('Install Beta Releases') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
<button on-click="_checkForUpdates" class="tap">[[ \$l("Check For Updates...") ]]</button>
</section>
<section hidden\$="[[ !_isDesktop() ]]">
<div class="section-header">[[ \$l("Database") ]]</div>
<div class="info">
<div>[[ \$l("Current Location:") ]]</div>
<div class="db-path">[[ _dbPath ]]</div>
</div>
<button on-click="_saveDBAs" class="tap">[[ \$l("Change Location...") ]]</button>
<button on-click="_loadDB" class="tap">[[ \$l("Load Different Database...") ]]</button>
</section>
<section>
<button class="info tap" on-click="_openSource">
<div><strong>Padlock {{ settings.version }}</strong></div>
<div class="made-in">Made with in Germany</div>
</button>
<button on-click="_openWebsite" class="tap">[[ \$l("Website") ]]</button>
<button on-click="_sendMail" class="tap">[[ \$l("Support") ]]</button>
<button on-click="_promptReview" class="tap">
<span>[[ \$l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
</button>
</section>
</main>
<div class="rounded-corners"></div>
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden="">
`;
}
static get is() { return "pl-settings-view"; }
connectedCallback() {
super.connectedCallback();
if (isElectron()) {
const desktopSettings = getDesktopSettings().get();
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
this._dbPath = desktopSettings.dbPath;
}
}
animate() {
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
}
_back() {
this.dispatchEvent(new CustomEvent("settings-back"));
}
//* Opens the change password dialog and resets the corresponding input elements
_changePassword() {
let newPwd;
return this.promptPassword(this.password, $l(
"Are you sure you want to change your master password? Enter your " +
"current password to continue!"
), $l("Confirm"), $l("Cancel"))
.then((success) => {
if (success) {
return this.prompt($l("Now choose a new master password!"),
$l("Enter New Password"), "password", $l("Confirm"), $l("Cancel"), false, (val) => {
if (val === "") {
return Promise.reject($l("Please enter a password!"));
}
return Promise.resolve(val);
});
} else {
return Promise.reject();
}
})
.then((pwd) => {
if (pwd === null) {
return Promise.reject();
}
newPwd = pwd;
return this.promptPassword(pwd, $l("Confirm your new master password!"),
$l("Confirm"), $l("Cancel"));
})
.then((success) => {
if (success) {
return this.setPassword(newPwd);
} else {
return Promise.reject();
}
})
.then(() => {
if (this.settings.syncConnected) {
return this.confirm(
$l("Do you want to update the password for you online account {0} as well?",
this.settings.syncEmail),
$l("Yes"), $l("No")
).then((confirmed) => {
if (confirmed) {
return this.setRemotePassword(this.password);
}
});
}
})
.then(() => {
this.alert($l("Master password changed successfully."), { type: "success" });
});
}
_openWebsite() {
window.open("https://padlock.io", "_system");
}
_sendMail() {
window.open("mailto:support@padlock.io", "_system");
}
_openGithub() {
window.open("https://github.com/maklesoft/padlock/", "_system");
}
_resetData() {
this.promptPassword(this.password,
$l(
"Are you sure you want to delete all your data and reset the app? Enter your " +
"master password to continue!"
),
$l("Reset App")
)
.then((success) => {
if (success) {
return this.resetData();
}
});
}
_import() {
const options = [
$l("From Clipboard")
];
if (!isCordova()) {
options.push($l("From File"));
}
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" })
.then((choice) => {
switch (choice) {
case 0:
this._importFromClipboard();
break;
case 1:
this.$.importFile.click();
break;
}
});
}
_importFile() {
const file = this.$.importFile.files[0];
const reader = new FileReader();
reader.onload = () => {
this._importString(reader.result)
.catch((e) => {
switch (e.code) {
case "decryption_failed":
this.alert($l("Failed to open file. Did you enter the correct password?"),
{ type: "warning" });
break;
case "unsupported_container_version":
this.confirm($l(
"It seems the data you are trying to import was exported from a " +
"newer version of Padlock and can not be opened with the version you are " +
"currently running."
), $l("Check For Updates"), $l("Cancel"), { type: "info" })
.then((confirmed) => {
if (confirmed) {
padlock.platform.checkForUpdates();
}
});
break;
case "invalid_csv":
this.alert($l("Failed to recognize file format."), { type: "warning" });
break;
default:
this.alert($l("Failed to open file."), { type: "warning" });
throw e;
}
});
this.$.importFile.value = "";
};
reader.readAsText(file);
}
_importFromClipboard() {
padlock.platform.getClipboard()
.then((str) => this._importString(str))
.catch((e) => {
switch (e.code) {
case "decryption_failed":
this.alert($l("Failed to decrypt data. Did you enter the correct password?"),
{ type: "warning" });
break;
default:
this.alert($l("No supported data found in clipboard. Please make sure to copy " +
"you data to the clipboard first (e.g. via ctrl + C)."), { type: "warning" });
}
});
}
_importString(rawStr) {
const imp = padlock.imp;
const isPadlock = imp.isFromPadlock(rawStr);
const isSecuStore = imp.isFromSecuStore(rawStr);
const isLastPass = imp.isFromLastPass(rawStr);
const isCSV = imp.isCSV(rawStr);
return Promise.resolve()
.then(() => {
if (isPadlock || isSecuStore) {
return this.prompt($l("This file is protected by a password."),
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"));
}
})
.then((pwd) => {
if (pwd === null) {
return;
}
if (isPadlock) {
return imp.fromPadlock(rawStr, pwd);
} else if (isSecuStore) {
return imp.fromSecuStore(rawStr, pwd);
} else if (isLastPass) {
return imp.fromLastPass(rawStr);
} else if (isCSV) {
return this.choose($l(
"The data you want to import seems to be in CSV format. Before you continue, " +
"please make sure that the data is structured according to Padlocks specific " +
"requirements!"
), [$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")], { type: "info" })
.then((choice) => {
switch (choice) {
case 0:
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
// Reopen dialog for when the user comes back from the web page
return this._importString(rawStr);
case 1:
return imp.fromCSV(rawStr);
case 2:
return;
}
});
} else {
throw new imp.ImportError("invalid_csv");
}
})
.then((records) => {
if (records) {
this.addRecords(records);
this.dispatch("data-imported", { records: records });
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
}
});
}
_isMobile() {
return isCordova();
}
_isTouch() {
return isTouch();
}
_openSource() {
window.open("https://github.com/maklesoft/padlock/", "_system");
}
_autoLockInfo() {
return this.$l(
"Tell Padlock to automatically lock the app after a certain period of " +
"inactivity in case you leave your device unattended for a while."
);
}
_peekValuesInfo() {
return this.$l(
"If enabled allows peeking at field values in the record list " +
"by moving the mouse cursor over the corresponding field."
);
}
_resetDataInfo() {
return this.$l(
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
"and will restore the app to the state it was when you first launched it."
);
}
_promptReview() {
this.choose($l(
"So glad to hear you like our app! Would you mind taking a second to " +
"let others know what you think about Padlock?"
), [$l("Rate Padlock"), $l("No Thanks")])
.then((choice) => {
if (choice === 0) {
getReviewLink(0).then((link) => window.open(link, "_system"));
}
});
}
_isDesktop() {
return isElectron();
}
_desktopSettingsChanged() {
getDesktopSettings().set({
autoDownloadUpdates: this.$.autoUpdatesButton.active,
allowPrerelease: this.$.betaReleasesButton.active
});
}
_checkForUpdates() {
checkForUpdates();
}
_saveDBAs() {
saveDBAs();
}
_loadDB() {
loadDB();
}
_export() {
const exportDialog = this.getSingleton("pl-export-dialog");
exportDialog.export(this.records);
}
_autoSyncInfoText() {
return $l(
"Enable Auto Sync to automatically synchronize your data with " +
"your Padlock online account every time you make a change!"
);
}
_customHostChanged() {
if (this.settings.syncCustomHost) {
this.confirm(
$l("Are you sure you want to use a custom server for synchronization? " +
"This option is only recommended for advanced users!"),
$l("Continue"))
.then((confirmed) => {
if (confirmed) {
this.settingChanged();
} else {
this.set("settings.syncCustomHost", false);
}
});
} else {
this.settingChanged();
}
}
}
window.customElements.define(SettingsView.is, SettingsView);

View File

@ -1,891 +0,0 @@
import '../../../../../node_modules/zxcvbn/dist/zxcvbn.js';
import '../../styles/shared.js';
import '../animation/animation.js';
import '../base/base.js';
import '../dialog/dialog-mixin.js';
import '../data/data.js';
import '../input/input.js';
import '../loading-button/loading-button.js';
import '../locale/locale.js';
const { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, BaseElement, SyncMixin } = padlock;
const { applyMixins, wait } = padlock.util;
const { isTouch, getAppStoreLink } = padlock.platform;
const { track } = padlock.tracking;
class StartView extends applyMixins(
BaseElement,
DataMixin,
LocaleMixin,
DialogMixin,
AnimationMixin,
SyncMixin
) {
static get template() {
return Polymer.html`
<style include="shared">
@keyframes reveal {
from { transform: translate(0, 30px); opacity: 0; }
to { opacity: 1; }
}
@keyframes fade {
to { transform: translate(0, -200px); opacity: 0; }
}
:host {
--color-background: var(--color-primary);
--color-foreground: var(--color-tertiary);
--color-highlight: var(--color-secondary);
@apply --fullbleed;
@apply --scroll;
color: var(--color-foreground);
display: flex;
flex-direction: column;
z-index: 5;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
transform: translate3d(0, 0, 0);
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
}
main {
@apply --fullbleed;
background: transparent;
min-height: 510px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.form-box {
width: 300px;
border-radius: 12px;
overflow: hidden;
display: flex;
margin-top: 20px;
transform: translate3d(0, 0, 0);
}
.hero {
display: block;
font-size: 110px;
height: 120px;
width: 120px;
margin-bottom: 30px;
color: rgba(255, 255, 255, 0.9);
}
.welcome-title {
font-size: 120%;
font-weight: bold;
padding: 10px;
}
.welcome-subtitle {
width: 300px;
padding: 10px;
}
.start-button {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: bold;
height: auto;
}
.start-button pl-icon {
position: relative;
top: 2px;
width: 30px;
}
.form-box pl-input, .form-box .input-wrapper {
flex: 1;
text-align: center;
}
.form-box pl-loading-button {
width: var(--row-height);
}
.strength-meter {
font-size: 12px;
font-weight: bold;
margin-top: 10px;
margin-bottom: -15px;
height: 16px;
}
.hint {
font-size: var(--font-size-tiny);
width: 305px;
margin-top: 40px;
}
.hint pl-icon {
width: 1em;
height: 1em;
vertical-align: middle;
}
:host(:not([_mode="get-started"])) .get-started,
:host(:not([_mode="unlock"])) .unlock {
display: none;
}
.get-started-steps {
width: 100%;
height: 270px;
position: relative;
}
.get-started-step {
@apply --fullbleed;
display: flex;
flex-direction: column;
align-items: center;
}
.get-started-step:not(.center) {
pointer-events: none;
}
.get-started-step > * {
transform: translate3d(0, 0, 0);
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
}
.get-started-step > :nth-child(2) {
transition-delay: 0.1s;
}
.get-started-step > :nth-child(3) {
transition-delay: 0.2s;
}
.get-started-step:not(.center) > * {
opacity: 0;
}
.get-started-step.left > * {
transform: translate3d(-200px, 0, 0);
}
.get-started-step.right > * {
transform: translate3d(200px, 0, 0);
}
.get-started-thumbs {
position: absolute;
bottom: 20px;
display: flex;
justify-content: center;
width: 100%;
}
.get-started-thumbs > * {
background: var(--color-foreground);
width: 10px;
height: 10px;
border-radius: 100%;
margin: 5px;
cursor: pointer;
}
.get-started-thumbs > .right {
opacity: 0.3;
}
button.skip {
background: none;
border: none;
height: auto;
line-height: normal;
font-weight: bold;
margin-top: 40px;
}
.version {
position: absolute;
left: 0;
right: 0;
bottom: 20px;
margin: auto;
font-size: var(--font-size-small);
color: rgba(0, 0, 0, 0.25);
text-shadow: none;
cursor: pointer;
}
.hint.choose-password {
margin-top: 30px;
width: 160px;
text-decoration: underline;
font-weight: bold;
cursor: pointer;
}
:host([open]) {
pointer-events: none;
}
:host([open]) {
transition-delay: 0.4s;
transform: translate3d(0, -100%, 0);
}
</style>
<main class="unlock">
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
<div class="form-box tiles-2 animate-in animate-out">
<pl-input id="passwordInput" type="password" class="tap" select-on-focus="" on-enter="_unlock" no-tab="[[ open ]]" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap" label="[[ \$l('Unlock') ]]" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</main>
<main class="get-started">
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
<div class="get-started-steps">
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
<div class="welcome-title animate-in">[[ \$l("Welcome to Padlock!") ]]</div>
<div class="welcome-subtitle animate-in">[[ \$l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
<div>[[ \$l("Get Started") ]]</div>
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
<div class="form-box tiles-2">
<pl-input id="emailInput" type="email" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterEmail" placeholder="[[ \$l('Enter Email Address') ]]"></pl-input>
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="cloud"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
</div>
<button class="skip" on-click="_skipEmail">[[ \$l("Use Offline") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
<div class="form-box tiles-2">
<pl-input id="codeInput" required="" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterCode" placeholder="[[ \$l('Enter Login Code') ]]"></pl-input>
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="mail"></pl-icon>
<span inner-h-t-m-l="[[ \$l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
</div>
<button class="skip" on-click="_cancelActivation">[[ \$l("Cancel") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ !_hasCloudData ]]">
<div>
<div class="form-box tiles-2">
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterCloudPassword" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ \$l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
</div>
<button class="skip" on-click="_forgotCloudPassword">[[ \$l("I Forgot My Password") ]]</button>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ _hasCloudData ]]">
<div>
<div class="form-box tiles-2">
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="strength-meter">[[ _pwdStrength ]]</div>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
</div>
<div on-click="_openPwdHowto" class="hint choose-password">[[ \$l("How do I choose a good master password?") ]]</div>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
<div class="form-box tiles-2">
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_confirmNewPassword" placeholder="[[ \$l('Confirm Master Password') ]]"></pl-input>
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
<div class="hint">
<pl-icon icon="lock"></pl-icon>
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]" <="" span="">
</span></div>
</div>
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
<div class="welcome-title animate-out">[[ \$l("All done!") ]]</div>
<div class="welcome-subtitle animate-out">[[ \$l("You're all set! Enjoy using Padlock!") ]]</div>
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
<span>[[ \$l("Finish Setup") ]]</span>
<pl-icon icon="forward"></pl-icon>
</pl-loading-button>
</div>
</div>
<div class="get-started-thumbs animate-in animate-out">
<div class\$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
<div class\$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
</div>
</main>
`;
}
static get is() { return "pl-start-view"; }
static get properties() { return {
open: {
type: Boolean,
value: false,
reflectToAttribute: true,
observer: "_openChanged"
},
_getStartedStep: {
type: Number,
value: 0
},
_hasData: {
type: Boolean
},
_hasCloudData: {
type: Boolean,
value: false
},
_mode: {
type: String,
reflectToAttribute: true,
computed: "_computeMode(_hasData)"
},
_pwdStrength: {
type: String,
computed: "_passwordStrength(newPwd)"
}
}; }
constructor() {
super();
this._failCount = 0;
}
ready() {
super.ready();
this.dataReady()
.then(() => this.reset())
.then(() => {
if (!isTouch() && this._hasData) {
this.$.passwordInput.focus();
}
this._openChanged();
});
}
reset() {
this.$.passwordInput.value = "";
this.$.emailInput.value = "";
this.$.newPasswordInput.value = "";
this.$.confirmPasswordInput.value = "";
this.$.cloudPwdInput.value = "";
this.$.unlockButton.stop();
this.$.getStartedButton.stop();
this._failCount = 0;
this._getStartedStep = 0;
this._hasCloudData = false;
return this._checkHasData();
}
focus() {
this.$.passwordInput.focus();
}
_openChanged() {
if (this.open) {
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
animation: "fade",
duration: 400,
fullDuration: 600,
initialDelay: 0,
fill: "forwards",
easing: "cubic-bezier(1, 0, 0.2, 1)",
clear: 3000
});
} else {
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
animation: "reveal",
duration: 1000,
fullDuration: 1500,
initialDelay: 300,
fill: "backwards",
clear: 3000
});
}
}
_startSetup() {
this._getStartedStep = 1;
if (!isTouch()) {
this.$.emailInput.focus();
}
track("Setup: Start");
}
_enterEmail() {
this.$.emailInput.blur();
if (this.$.emailInput.invalid) {
this.alert(
this.$.emailInput.validationMessage || $l("Please enter a valid email address!"),
{ type: "warning" }
).then(() => this.$.emailInput.focus());
return;
}
this.$.emailButton.start();
padlock.stats.set({ pairingSource: "Setup" });
this.connectCloud(this.$.emailInput.value)
.then(() => {
this.$.emailButton.success();
this._getStartedStep = 2;
this.$.codeInput.value = "";
if (!isTouch()) {
this.$.codeInput.focus();
}
})
.catch(() => this.$.emailButton.fail());
track("Setup: Email", { "Skipped": false, "Email": this.$.emailInput.value });
}
_enterCode() {
if (this._checkingCode) {
return;
}
if (this.$.codeInput.invalid) {
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
return;
}
this._checkingCode = true;
this.$.codeButton.start();
this.activateToken(this.$.codeInput.value)
.then((success) => {
if (success) {
return this.hasCloudData().then((hasData) => {
this._checkingCode = false;
this.$.codeButton.success();
this._hasCloudData = hasData;
return this._connected();
});
} else {
this._checkingCode = false;
this._rumble();
this.$.codeButton.fail();
}
})
.catch((e) => {
this._checkingCode = false;
this._rumble();
this.$.codeButton.fail();
this._handleCloudError(e);
});
track("Setup: Code", { "Email": this.$.emailInput.value });
}
_connected() {
setTimeout(() => {
this._getStartedStep = 3;
if (!isTouch()) {
this.$.newPasswordInput.focus();
}
}, 50);
}
_cancelActivation() {
this.cancelConnect();
this.$.codeInput.value = "";
this._getStartedStep = 1;
}
_skipEmail() {
this.$.emailInput.value = "";
this._getStartedStep = 3;
if (!isTouch()) {
this.$.newPasswordInput.focus();
}
track("Setup: Email", { "Skipped": true });
}
_enterNewPassword() {
this.$.newPasswordInput.blur();
const pwd = this.$.newPasswordInput.value;
if (!pwd) {
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
return;
}
const next = () => {
this._getStartedStep = 4;
if (!isTouch()) {
this.$.confirmPasswordInput.focus();
}
track("Setup: Choose Password");
};
if (zxcvbn(pwd).score < 2) {
this.choose($l(
"The password you entered is weak which makes it easier for attackers to break " +
"the encryption used to protect your data. Try to use a longer password or include a " +
"variation of uppercase, lowercase and special characters as well as numbers!"
), [
$l("Learn More"),
$l("Choose Different Password"),
$l("Use Anyway")
], {
type: "warning",
title: $l("WARNING: Weak Password"),
hideIcon: true
}).then((choice) => {
switch (choice) {
case 0:
this._openPwdHowto();
break;
case 1:
this.$.newPasswordInput.focus();
break;
case 2:
next();
break;
}
});
return;
}
next();
}
_confirmNewPassword() {
this.$.confirmPasswordInput.blur();
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
this.choose($l(
"The password you entered does not match the original one!"
), [
$l("Try Again"),
$l("Change Password")
], { type: "warning" })
.then((choice) => {
switch (choice) {
case 0:
this.$.confirmPasswordInput.focus();
break;
case 1:
this._getStartedStep = 3;
this.$.newPasswordInput.focus();
break;
}
});
return;
}
this._getStartedStep = 5;
track("Setup: Confirm Password");
}
_computeMode() {
return this._hasData ? "unlock" : "get-started";
}
_checkHasData() {
return this.hasData()
.then((has) => this._hasData = has);
}
_finishSetup() {
this._initializeData();
track("Setup: Finish");
}
_initializeData() {
this.$.getStartedButton.start();
if (this._initializing) {
return;
}
this._initializing = true;
const password = this.cloudSource.password || this.$.newPasswordInput.value;
this.cloudSource.password = password;
const promises = [
this.initData(password),
wait(1000)
];
if (this.settings.syncConnected && !this._hasCloudData) {
promises.push(this.collection.save(this.cloudSource));
}
Promise.all(promises)
.then(() => {
this.$.getStartedButton.success();
this.$.newPasswordInput.blur();
this._initializing = false;
});
}
_promptResetData(message) {
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App"))
.then((value) => {
if (value == null) {
return;
}
if (value.toUpperCase() === "RESET") {
this.resetData();
} else {
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
}
});
}
_unlock() {
const password = this.$.passwordInput.value;
if (!password) {
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
return;
}
this.$.passwordInput.blur();
this.$.unlockButton.start();
if (this._unlocking) {
return;
}
this._unlocking = true;
Promise.all([
this.loadData(password),
wait(1000)
])
.then(() => {
this.$.unlockButton.success();
this._unlocking = false;
})
.catch((e) => {
this.$.unlockButton.fail();
this._unlocking = false;
switch (e.code) {
case "decryption_failed":
this._rumble();
this._failCount++;
if (this._failCount > 2) {
this.promptForgotPassword()
.then((doReset) => {
if (doReset) {
this.resetData();
}
});
} else {
this.$.passwordInput.focus();
}
break;
case "unsupported_container_version":
this.confirm($l(
"It seems the data stored on this device was saved with a newer version " +
"of Padlock and can not be opened with the version you are currently running. " +
"Please install the latest version of Padlock or reset the data to start over!"
), $l("Check For Updates"), $l("Reset Data"))
.then((confirmed) => {
if (confirmed) {
padlock.platform.checkForUpdates();
} else {
this._promptResetData($l(
"Are you sure you want to reset the app? " +
"WARNING: This will delete all your data!"
));
}
});
break;
default:
this._promptResetData($l(
"An error occured while loading your data! If the problem persists, please try " +
"resetting or reinstalling the app!"
));
throw e;
}
});
}
_passwordStrength(pwd) {
const score = pwd ? zxcvbn(pwd).score : -1;
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
return strength && $l("strength: {0}", strength);
}
_getStartedHint(step) {
return [
$l(
"Logging in will unlock advanced features like automatic backups and seamless " +
"synchronization between all your devices!"
),
$l(
"Your **master password** is a single passphrase used to protect your data. " +
"Without it, nobody will be able to access your data - not even us!"
),
$l(
"**Don't forget your master password!** For privacy and security reasons we never store your " +
"password anywhere which means we won't be able to help you recover your data in case you forget " +
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
)
][step];
}
_getStartedClass(currStep, step) {
return currStep > step ? "left" : currStep < step ? "right" : "center";
}
_goToStep(e) {
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
if (s < this._getStartedStep) {
this._getStartedStep = s;
}
}
_openProductPage() {
getAppStoreLink().then((link) => window.open(link, "_system"));
}
_openPwdHowto() {
window.open("https://padlock.io/howto/choose-master-password/", "_system");
}
_rumble() {
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`),
{ animation: "rumble", duration: 200, clear: true });
}
_enterCloudPassword() {
if (this._restoringCloud) {
return;
}
const password = this.$.cloudPwdInput.value;
if (!password) {
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
return;
}
this.$.cloudPwdInput.blur();
this.$.cloudPwdButton.start();
this._restoringCloud = true;
this.cloudSource.password = password;
this.collection.fetch(this.cloudSource)
.then(() => {
this.$.cloudPwdButton.success();
this._restoringCloud = false;
this._getStartedStep = 5;
})
.catch((e) => {
this.$.cloudPwdButton.fail();
this._restoringCloud = false;
if (e.code === "decryption_failed") {
this._rumble();
} else {
this._handleCloudError(e);
}
});
track("Setup: Remote Password", { Email: this.settings.syncEmail });
}
_forgotCloudPassword() {
this.forgotCloudPassword().then(() => {
this._hasCloudData = false;
this._connected();
});
}
}
window.customElements.define(StartView.is, StartView);

12
package-lock.json generated
View File

@ -218,6 +218,12 @@
"integrity": "sha512-PBHCvO98hNec9A491vBbh0ZNDOVxccwKL1u2pm6fs9oDgm7SEnw0lEHqHfjsYryDxnE3zaf7LvERWEXjOp1hig==",
"dev": true
},
"@types/zxcvbn": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz",
"integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==",
"dev": true
},
"@webcomponents/shadycss": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.2.1.tgz",
@ -8132,9 +8138,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"semver-diff": {
"version": "2.1.0",

View File

@ -17,6 +17,7 @@
"@types/moment-duration-format": "^2.2.0",
"@types/papaparse": "^4.1.28",
"@types/semver": "^5.3.33",
"@types/zxcvbn": "^4.4.0",
"archiver": "^1.2.0",
"bower": "^1.8.0",
"browserify": "^13.1.1",
@ -39,7 +40,6 @@
"polymer-analyzer": "^2.2.0",
"polymer-bundler": "^2.3.1",
"rimraf": "^2.5.4",
"semver": "^5.4.1",
"st": "^1.2.0",
"tsify": "^2.0.3",
"typescript": "^2.8.3",
@ -58,12 +58,12 @@
"moment": "^2.21.0",
"moment-duration-format": "^2.2.2",
"papaparse": "^4.1.2",
"semver": "^5.5.0",
"uuid": "^3.1.0",
"yargs": "^4.8.1",
"zxcvbn": "^4.4.2"
},
"scripts": {
"bower-install": "pushd app && bower install && popd",
"compile": "gulp compile --silent",
"lint": "eslint app/**/*.{js,html}",
"test": "karma start --single-run --browsers ChromeHeadless karma.conf.js",
@ -71,6 +71,6 @@
"build:mac": "gulp build --mac --silent",
"build:win": "gulp build --win --silent",
"build:linux": "gulp build --linux --silent",
"postinstall": "npm run bower-install && npm run compile && mkdir -p cordova/www"
"postinstall": "npm run compile && mkdir -p cordova/www"
}
}

View File

@ -19,14 +19,10 @@
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"*": ["typings/*"]
"*": ["typings/*"],
"zxcvbn/dist/zxcvbn": ["node_modules/@types/zxcvbn"]
}
},
"include": [
"app/src/**/*.ts",
"typings/*"
],
"exclude": [
"node_modules/**/*.ts"
]
"include": ["app/src/**/*.ts", "typings/*"],
"exclude": ["node_modules/**/*.ts"]
}