From 3fecb4f180804acfff091e6711c3a6e521e47702 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Apr 2018 11:54:53 +0100 Subject: [PATCH 01/47] Forcing z-index is not needed anymore. --- services/web/public/stylesheets/app/editor.less | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 9a8bdc5594..9eedb842a5 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -305,7 +305,6 @@ } .ui-layout-resizer when (@is-overleaf = true) { - z-index: 5 !important; width: @ui-resizer-size !important; background-color: @editor-resizer-bg-color; &.ui-layout-resizer-closed { From fba5e4f80a9f59fabf61a10fe5b3cedc3eab9124 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Apr 2018 11:28:21 +0100 Subject: [PATCH 02/47] Disable webpack source map --- services/web/webpack.config.prod.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/webpack.config.prod.js b/services/web/webpack.config.prod.js index 70287d543e..530af759fe 100644 --- a/services/web/webpack.config.prod.js +++ b/services/web/webpack.config.prod.js @@ -5,8 +5,7 @@ const merge = require('webpack-merge') const base = require('./webpack.config') module.exports = merge(base, { - // Enable a full source map. - devtool: 'source-map', + devtool: false, output: { // Override output path to minjs dir From 315b2f24eebe0bbf5be61db843b5cdeb06ef2480 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Apr 2018 16:01:59 +0100 Subject: [PATCH 03/47] Always include RT styles --- services/web/public/stylesheets/_style_includes.less | 6 +----- services/web/public/stylesheets/app/editor.less | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index de6c4ff7fe..7f6070e69c 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -84,8 +84,4 @@ @import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css"; @import "../js/libs/pdfListView/HighlightsLayer.css"; - -// CodeMirror -& when (@show-rich-text) { - @import "vendor/codemirror.css"; -} +@import "vendor/codemirror.css"; diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 9a8bdc5594..50d47e3d61 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -12,9 +12,7 @@ @import "./editor/online-users.less"; @import "./editor/hotkeys.less"; @import "./editor/review-panel.less"; -& when (@show-rich-text) { - @import "./editor/rich-text.less"; -} +@import "./editor/rich-text.less"; @ui-layout-toggler-def-height: 50px; @ui-resizer-size: 7px; From d433a881583d62b662947db216587524b1ae483e Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Apr 2018 16:58:12 +0100 Subject: [PATCH 04/47] Pass rich text flag if query string is set --- .../web/app/coffee/Features/Project/ProjectController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index cd3b3d1905..e95d208900 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -301,6 +301,7 @@ module.exports = ProjectController = themes: THEME_LIST maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display + showRichText: req.query.rt == 'true' timer.done() _buildProjectList: (allProjects, v1Projects = [])-> From c3b2af4345bcba69bb74ea0f7fbf59443eb6ee85 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Apr 2018 16:59:06 +0100 Subject: [PATCH 05/47] Instead of checking RT flag at compile time, adjust at run time --- services/web/app/views/project/editor/editor.pug | 1 + services/web/public/stylesheets/app/editor.less | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 2b42cda965..b1570fa55d 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -39,6 +39,7 @@ div.full-size( ace-editor="editor", ng-if="!editor.richText", ng-show="!!editor.sharejs_doc && !editor.opening", + style=showRichText ? "top: 40px" : "", theme="settings.theme", keybindings="settings.mode", font-size="settings.fontSize", diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 50d47e3d61..6350a00c24 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -80,10 +80,7 @@ .full-size; } -#editor when (@show-rich-text = true) { - top: 40px; // TODO: replace with toolbar height var -} -#editor-rich-text when (@show-rich-text = true) { +#editor-rich-text { top: 40px; // TODO: replace with toolbar height var } From eb05be6eb3da87be7cd7837a2e06c9f6acce57b7 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 6 Apr 2018 17:00:27 +0100 Subject: [PATCH 06/47] Remove unused feature flag --- services/web/app/coffee/infrastructure/Features.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 5dfe73f05f..519cfda6da 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -14,8 +14,6 @@ module.exports = Features = return Settings.enableGithubSync when 'v1-return-message' return Settings.accountMerge? and Settings.overleaf? - when 'rich-text' - return Settings.showRichText when 'custom-togglers' return Settings.overleaf? else From baec4eb195365cc70ffdc906e7170eaeec192047 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 22 Mar 2018 16:58:29 +0000 Subject: [PATCH 07/47] move uploads outside the lock --- .../Project/ProjectEntityUpdateHandler.coffee | 105 ++++++++++++------ .../ProjectEntityUpdateHandlerTests.coffee | 33 ++++-- 2 files changed, 97 insertions(+), 41 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index ff09eda64d..f87fcb389a 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -24,12 +24,23 @@ wrapWithLock = (methodWithoutLock) -> # This lock is used to make sure that the project structure updates are made # sequentially. In particular the updates must be made in mongo and sent to # the doc-updater in the same order. - methodWithLock = (project_id, args..., callback) -> - LockManager.runWithLock LOCK_NAMESPACE, project_id, - (cb) -> methodWithoutLock project_id, args..., cb - callback - methodWithLock.withoutLock = methodWithoutLock - methodWithLock + if typeof methodWithoutLock is 'function' + methodWithLock = (project_id, args..., callback) -> + LockManager.runWithLock LOCK_NAMESPACE, project_id, + (cb) -> methodWithoutLock project_id, args..., cb + callback + methodWithLock.withoutLock = methodWithoutLock + methodWithLock + else + # handle case with separate setup and locked stages + wrapWithSetup = methodWithoutLock.beforeLock # a function to set things up before the lock + mainTask = methodWithoutLock.withLock # function to execute inside the lock + methodWithLock = wrapWithSetup (project_id, args..., callback) -> + LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) -> + mainTask(project_id, args..., cb) + callback) + methodWithLock.withoutLock = wrapWithSetup mainTask + methodWithLock module.exports = ProjectEntityUpdateHandler = self = # this doesn't need any locking because it's only called by ProjectDuplicator @@ -129,21 +140,43 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(error) if error? callback null, doc, folder_id - addFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id) ->)-> - self.addFileWithoutUpdatingHistory.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, folder_id, path, fileStoreUrl) -> - return callback(error) if error? - newFiles = [ - file: fileRef - path: path - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> - return callback(error) if error? - callback null, fileRef, folder_id + addFile: wrapWithLock + beforeLock: (next) -> + (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id) ->)-> + if not SafePath.isCleanFilename fileName + return callback new Errors.InvalidNameError("invalid element name") + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) + withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> + ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" + return callback(err) + TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> + return callback(err) if err? + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) - replaceFile: wrapWithLock (project_id, file_id, fsPath, linkedFileData, userId, callback)-> - FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> - return callback(err) if err? + replaceFile: wrapWithLock + beforeLock: (next) -> + (project_id, file_id, fsPath, linkedFileData, userId, callback)-> + FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> + return callback(err) if err? + next project_id, file_id, fsPath, linkedFileData, userId, fileStoreUrl, callback + withLock: (project_id, file_id, fsPath, linkedFileData, userId, fileStoreUrl, callback)-> ProjectEntityMongoUpdateHandler.replaceFile project_id, file_id, linkedFileData, (err, fileRef, project, path) -> return callback(err) if err? newFiles = [ @@ -180,22 +213,26 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) if err? callback(null, doc, folder_id, result?.path?.fileSystem) - addFileWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> - # This method should never be called directly, except when importing a project - # from Overleaf. It skips sending updates to the project history, which will break - # the history unless you are making sure it is updated in some other way. + addFileWithoutUpdatingHistory: wrapWithLock + beforeLock: (next) -> + (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> + # This method should never be called directly, except when importing a project + # from Overleaf. It skips sending updates to the project history, which will break + # the history unless you are making sure it is updated in some other way. - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") + if not SafePath.isCleanFilename fileName + return callback new Errors.InvalidNameError("invalid element name") - fileRef = new File( - name: fileName - linkedFileData: linkedFileData - ) - FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) + withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> if err? logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index 9d3651f55d..766e360840 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -295,14 +295,33 @@ describe 'ProjectEntityUpdateHandler', -> beforeEach -> @path = "/path/to/file" - @newFile = _id: file_id - @ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory = - withoutLock: sinon.stub().yields(null, @newFile, folder_id, @path, @fileUrl) - @ProjectEntityUpdateHandler.addFile project_id, folder_id, @docName, @fileSystemPath, @linkedFileData, userId, @callback + @newFile = {_id: file_id, rev: 0, name: @fileName} + @TpdsUpdateSender.addFile = sinon.stub().yields() + @ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project) + @ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback - it "creates the doc without history", () -> - @ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory.withoutLock - .calledWith(project_id, folder_id, @docName, @fileSystemPath, @linkedFileData, userId) + it "updates the file in the filestore", () -> + @FileStoreHandler.uploadFileFromDisk + .calledWith(project_id, file_id, @fileSystemPath) + .should.equal true + + it "updates the file in mongo", () -> + fileMatcher = sinon.match (file) => + file.name == @fileName + + @ProjectEntityMongoUpdateHandler.addFile + .calledWithMatch(project_id, folder_id, fileMatcher) + .should.equal true + + it "notifies the tpds", () -> + @TpdsUpdateSender.addFile + .calledWith({ + project_id: project_id + project_name: @project.name + file_id: file_id + rev: 0 + path: @path + }) .should.equal true it "sends the change in project structure to the doc updater", () -> From cdfa2de68d95ecb57cdefff69d67987aa8431aa6 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 23 Mar 2018 15:07:08 +0000 Subject: [PATCH 08/47] add comment about upsert and locking --- .../Features/Project/ProjectEntityUpdateHandler.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index f87fcb389a..2596a0f514 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -261,6 +261,11 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) if err? callback null, doc, !existingDoc? + # FIXME: this method needs to be changed, currently it needs a lock around an upload to S3 because + # there is no way to know in advance if the file is a replace or an insert (at least you have to + # take the lock to find that out and do any operation in that lock). In the new scheme any upsert + # should always create the new file (outside the lock) in S3 first and then make the changes to the + # project in a lock, marking the old file as deleted if it exists. upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (err, file, isNewFile)->)-> ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> return callback(error) if error? From d949338ed4b285850f1361a936f0447eb991baf1 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 26 Mar 2018 11:44:38 +0100 Subject: [PATCH 09/47] separate out file uploads --- .../Project/ProjectEntityUpdateHandler.coffee | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 2596a0f514..d09baee1d2 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -140,19 +140,24 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(error) if error? callback null, doc, folder_id + uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, fileStoreUrl) ->)-> + if not SafePath.isCleanFilename fileName + return callback new Errors.InvalidNameError("invalid element name") + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + callback(null, fileRef, fileStoreUrl) + addFile: wrapWithLock beforeLock: (next) -> - (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id) ->)-> - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - fileRef = new File( - name: fileName - linkedFileData: linkedFileData - ) - FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) + (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> + ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> + return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> @@ -213,24 +218,14 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) if err? callback(null, doc, folder_id, result?.path?.fileSystem) - addFileWithoutUpdatingHistory: wrapWithLock + addFileWithoutUpdatingHistory: wrapWithLock + # This method should never be called directly, except when importing a project + # from Overleaf. It skips sending updates to the project history, which will break + # the history unless you are making sure it is updated in some other way. beforeLock: (next) -> - (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> - # This method should never be called directly, except when importing a project - # from Overleaf. It skips sending updates to the project history, which will break - # the history unless you are making sure it is updated in some other way. - - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - - fileRef = new File( - name: fileName - linkedFileData: linkedFileData - ) - FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) + (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> + ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> + return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> From fe8897d757c9181d3e4d4ab47fdd5223cad001e2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 26 Mar 2018 12:32:04 +0100 Subject: [PATCH 10/47] refactor addFile and addFileWithOutUpdatingHistory --- .../Project/ProjectEntityUpdateHandler.coffee | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index d09baee1d2..1df0f2196c 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -153,6 +153,15 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) callback(null, fileRef, fileStoreUrl) + _addFileAndSendToTpds: (project_id, folder_id, fileName, fileRef, callback = (error) ->)-> + ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" + return callback(err) + TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> + return callback(err) if err? + callback(null, result, project) + addFile: wrapWithLock beforeLock: (next) -> (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> @@ -160,20 +169,16 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> - ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" - return callback(err) - TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> - return callback(err) if err? - newFiles = [ - file: fileRef - path: result?.path?.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> - return callback(error) if error? - callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) + ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileName, fileRef, (err, result, project) -> + return callback(err) if err? + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) replaceFile: wrapWithLock beforeLock: (next) -> @@ -228,11 +233,7 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> - ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" - return callback(err) - TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> + ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileName, fileRef, (err, result, project) -> return callback(err) if err? callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) From 61b192fb2c0d21415290265489d322cb05e5a3ef Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 26 Mar 2018 15:57:10 +0100 Subject: [PATCH 11/47] fix indentation of comment --- .../Features/Project/ProjectEntityUpdateHandler.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 1df0f2196c..358925865f 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -224,9 +224,9 @@ module.exports = ProjectEntityUpdateHandler = self = callback(null, doc, folder_id, result?.path?.fileSystem) addFileWithoutUpdatingHistory: wrapWithLock - # This method should never be called directly, except when importing a project - # from Overleaf. It skips sending updates to the project history, which will break - # the history unless you are making sure it is updated in some other way. + # This method should never be called directly, except when importing a project + # from Overleaf. It skips sending updates to the project history, which will break + # the history unless you are making sure it is updated in some other way. beforeLock: (next) -> (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> From b551f3c7e945d539a98f5c4a4602241aa882eaf1 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 26 Mar 2018 16:18:50 +0100 Subject: [PATCH 12/47] fix indentation --- .../Project/ProjectEntityUpdateHandler.coffee | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 358925865f..aa84ade434 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -141,17 +141,17 @@ module.exports = ProjectEntityUpdateHandler = self = callback null, doc, folder_id uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, fileStoreUrl) ->)-> - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - fileRef = new File( - name: fileName - linkedFileData: linkedFileData - ) - FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) - callback(null, fileRef, fileStoreUrl) + if not SafePath.isCleanFilename fileName + return callback new Errors.InvalidNameError("invalid element name") + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + callback(null, fileRef, fileStoreUrl) _addFileAndSendToTpds: (project_id, folder_id, fileName, fileRef, callback = (error) ->)-> ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> @@ -234,8 +234,8 @@ module.exports = ProjectEntityUpdateHandler = self = next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileName, fileRef, (err, result, project) -> - return callback(err) if err? - callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) + return callback(err) if err? + callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)-> ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> From e29c657c8a356491840d2dec81a6931a1f3f4efc Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 27 Mar 2018 09:41:38 +0100 Subject: [PATCH 13/47] preserve original callback signature for addFile --- .../coffee/Features/Project/ProjectEntityUpdateHandler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index aa84ade434..75ae3fe02a 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -178,7 +178,7 @@ module.exports = ProjectEntityUpdateHandler = self = ] DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> return callback(error) if error? - callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) + callback(null, fileRef, folder_id) replaceFile: wrapWithLock beforeLock: (next) -> From 38dab1c82c96fe81c86384229162628a75b40578 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 27 Mar 2018 09:44:33 +0100 Subject: [PATCH 14/47] clean up whitespace --- .../Project/ProjectEntityUpdateHandler.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 75ae3fe02a..33421d19ee 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -32,11 +32,11 @@ wrapWithLock = (methodWithoutLock) -> methodWithLock.withoutLock = methodWithoutLock methodWithLock else - # handle case with separate setup and locked stages + # handle case with separate setup and locked stages wrapWithSetup = methodWithoutLock.beforeLock # a function to set things up before the lock mainTask = methodWithoutLock.withLock # function to execute inside the lock methodWithLock = wrapWithSetup (project_id, args..., callback) -> - LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) -> + LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) -> mainTask(project_id, args..., cb) callback) methodWithLock.withoutLock = wrapWithSetup mainTask @@ -160,9 +160,9 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> return callback(err) if err? - callback(null, result, project) + callback(null, result, project) - addFile: wrapWithLock + addFile: wrapWithLock beforeLock: (next) -> (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> @@ -178,7 +178,7 @@ module.exports = ProjectEntityUpdateHandler = self = ] DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> return callback(error) if error? - callback(null, fileRef, folder_id) + callback(null, fileRef, folder_id) replaceFile: wrapWithLock beforeLock: (next) -> @@ -258,9 +258,9 @@ module.exports = ProjectEntityUpdateHandler = self = callback null, doc, !existingDoc? # FIXME: this method needs to be changed, currently it needs a lock around an upload to S3 because - # there is no way to know in advance if the file is a replace or an insert (at least you have to + # there is no way to know in advance if the file is a replace or an insert (at least you have to # take the lock to find that out and do any operation in that lock). In the new scheme any upsert - # should always create the new file (outside the lock) in S3 first and then make the changes to the + # should always create the new file (outside the lock) in S3 first and then make the changes to the # project in a lock, marking the old file as deleted if it exists. upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (err, file, isNewFile)->)-> ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> @@ -373,7 +373,7 @@ module.exports = ProjectEntityUpdateHandler = self = path: file.path url: FileStoreHandler._buildUrl(project_id, file.file._id) - DocumentUpdaterHandler.resyncProjectHistory project_id, docs, files, callback + DocumentUpdaterHandler.resyncProjectHistory project_id, docs, files, callback _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> if(entityType.indexOf("file") != -1) self._cleanUpFile project, entity, path, userId, callback From 1a68f768d44eb5936ee9988a0e508dbbf2f1a804 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 3 Apr 2018 15:00:34 +0100 Subject: [PATCH 15/47] rename internal method uploadFile to _uploadFile --- .../Features/Project/ProjectEntityUpdateHandler.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 33421d19ee..baa7213dd8 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -140,7 +140,7 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(error) if error? callback null, doc, folder_id - uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, fileStoreUrl) ->)-> + _uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, fileStoreUrl) ->)-> if not SafePath.isCleanFilename fileName return callback new Errors.InvalidNameError("invalid element name") fileRef = new File( @@ -165,7 +165,7 @@ module.exports = ProjectEntityUpdateHandler = self = addFile: wrapWithLock beforeLock: (next) -> (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> - ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> + ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> @@ -229,7 +229,7 @@ module.exports = ProjectEntityUpdateHandler = self = # the history unless you are making sure it is updated in some other way. beforeLock: (next) -> (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> - ProjectEntityUpdateHandler.uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> + ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) -> return callback(error) if error? next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> From 882b55543315c29f8896b7619973a3228cf39bb1 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 27 Mar 2018 12:08:30 +0100 Subject: [PATCH 16/47] create new file on replace --- .../DocumentUpdaterHandler.coffee | 27 ++-- .../Features/Editor/EditorController.coffee | 18 ++- .../ProjectEntityMongoUpdateHandler.coffee | 60 ++++++--- .../Project/ProjectEntityUpdateHandler.coffee | 122 ++++++++++-------- services/web/app/coffee/models/Project.coffee | 5 + .../coffee/ProjectDuplicateNameTests.coffee | 3 + .../coffee/ProjectStructureTests.coffee | 15 ++- .../Editor/EditorControllerTests.coffee | 12 +- ...rojectEntityMongoUpdateHandlerTests.coffee | 57 +++++++- .../ProjectEntityUpdateHandlerTests.coffee | 88 ++++++------- 10 files changed, 261 insertions(+), 146 deletions(-) diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index e340187dbb..9ecff2185e 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -174,6 +174,23 @@ module.exports = DocumentUpdaterHandler = oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString() newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString() + # Send deletes before adds (and renames) to keep a 1:1 mapping between + # paths and ids + # + # When a file is replaced, we first delete the old file and then add the + # new file. If the 'add' operation is sent to project history before the + # 'delete' then we would have two files with the same path at that point + # in time. + for id, oldEntity of oldEntitiesHash + newEntity = newEntitiesHash[id] + + if !newEntity? + # entity deleted + updates.push + id: id + pathname: oldEntity.path + newPathname: '' + for id, newEntity of newEntitiesHash oldEntity = oldEntitiesHash[id] @@ -191,16 +208,6 @@ module.exports = DocumentUpdaterHandler = pathname: oldEntity.path newPathname: newEntity.path - for id, oldEntity of oldEntitiesHash - newEntity = newEntitiesHash[id] - - if !newEntity? - # entity deleted - updates.push - id: id - pathname: oldEntity.path - newPathname: '' - updates PENDINGUPDATESKEY = "PendingUpdates" diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 7294e88bea..846042312d 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -41,11 +41,13 @@ module.exports = EditorController = callback err, doc upsertFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (err, file) ->) -> - ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, file, didAddFile) -> + ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile) -> return callback(err) if err? - if didAddFile - EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, file, source, linkedFileData - callback null, file + if not didAddFile # replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source + # now add the new file on the client + EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, newFile, source, linkedFileData + callback null, newFile upsertDocWithPath: (project_id, elementPath, docLines, source, user_id, callback) -> ProjectEntityUpdateHandler.upsertDocWithPath project_id, elementPath, docLines, source, user_id, (err, doc, didAddNewDoc, newFolders, lastFolder) -> @@ -57,12 +59,14 @@ module.exports = EditorController = callback() upsertFileWithPath: (project_id, elementPath, fsPath, linkedFileData, source, user_id, callback) -> - ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, file, didAddFile, newFolders, lastFolder) -> + ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile, newFolders, lastFolder) -> return callback(err) if err? EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> return callback(err) if err? - if didAddFile - EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, file, source, linkedFileData + if not didAddFile # replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source + # now add the new file on the client + EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, newFile, source, linkedFileData callback() addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)-> diff --git a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee index 63138d1ca0..57f65a67dc 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee @@ -48,28 +48,28 @@ module.exports = ProjectEntityMongoUpdateHandler = self = self._confirmFolder project, folder_id, (folder_id)-> self._putElement project, folder_id, fileRef, "file", callback - replaceFile: wrapWithLock (project_id, file_id, linkedFileData, callback) -> + replaceFileWithNew: wrapWithLock (project_id, file_id, newFileRef, callback) -> ProjectGetter.getProjectWithoutLock project_id, {rootFolder: true, name:true}, (err, project) -> return callback(err) if err? ProjectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> return callback(err) if err? - conditions = _id:project._id - inc = {} - inc["#{path.mongo}.rev"] = 1 - # currently we do not need to increment the project version number for changes that are replacements - # but when we make switch to having immutable files the replace operation will add a new file, and - # this will require a version increase. We will start incrementing the project version now as it does - # no harm and will help to test it. - inc['version'] = 1 - set = {} - set["#{path.mongo}.created"] = new Date() - set["#{path.mongo}.linkedFileData"] = linkedFileData - update = - "$inc": inc - "$set": set - Project.update conditions, update, {}, (err) -> + ProjectEntityMongoUpdateHandler._insertDeletedFileReference project_id, fileRef, (err) -> return callback(err) if err? - callback null, fileRef, project, path + conditions = _id:project._id + inc = {} + # increment the project structure version as we are adding a new file here + inc['version'] = 1 + set = {} + set["#{path.mongo}._id"] = newFileRef._id + set["#{path.mongo}.created"] = new Date() + set["#{path.mongo}.linkedFileData"] = newFileRef.linkedFileData + set["#{path.mongo}.rev"] = 1 + update = + "$inc": inc + "$set": set + Project.update conditions, update, {}, (err) -> + return callback(err) if err? + callback null, fileRef, project, path mkdirp: wrapWithLock (project_id, path, callback) -> folders = path.split('/') @@ -300,3 +300,29 @@ module.exports = ProjectEntityMongoUpdateHandler = self = if isNestedFolder return callback(new Errors.InvalidNameError("destination folder is a child folder of me")) callback() + + _insertDeletedDocReference: (project_id, doc, callback = (error) ->) -> + Project.update { + _id: project_id + }, { + $push: { + deletedDocs: { + _id: doc._id + name: doc.name + } + } + }, {}, callback + + _insertDeletedFileReference: (project_id, fileRef, callback = (error) ->) -> + Project.update { + _id: project_id + }, { + $push: { + deletedFiles: { + _id: fileRef._id + name: fileRef.name + linkedFileData: fileRef.linkedFileData + deletedAt: new Date() + } + } + }, {}, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index baa7213dd8..e3aea07456 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -40,6 +40,8 @@ wrapWithLock = (methodWithoutLock) -> mainTask(project_id, args..., cb) callback) methodWithLock.withoutLock = wrapWithSetup mainTask + methodWithLock.beforeLock = methodWithoutLock.beforeLock + methodWithLock.mainTask = methodWithoutLock.withLock methodWithLock module.exports = ProjectEntityUpdateHandler = self = @@ -183,20 +185,29 @@ module.exports = ProjectEntityUpdateHandler = self = replaceFile: wrapWithLock beforeLock: (next) -> (project_id, file_id, fsPath, linkedFileData, userId, callback)-> - FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> + # create a new file + fileRef = new File( + name: "dummy-upload-filename" + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> return callback(err) if err? - next project_id, file_id, fsPath, linkedFileData, userId, fileStoreUrl, callback - withLock: (project_id, file_id, fsPath, linkedFileData, userId, fileStoreUrl, callback)-> - ProjectEntityMongoUpdateHandler.replaceFile project_id, file_id, linkedFileData, (err, fileRef, project, path) -> + next project_id, file_id, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback + withLock: (project_id, file_id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback)-> + ProjectEntityMongoUpdateHandler.replaceFileWithNew project_id, file_id, newFileRef, (err, oldFileRef, project, path) -> return callback(err) if err? + oldFiles = [ + file: oldFileRef + path: path.fileSystem + ] newFiles = [ - file: fileRef + file: newFileRef path: path.fileSystem url: fileStoreUrl ] - TpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) -> + TpdsUpdateSender.addFile {project_id:project._id, file_id:newFileRef._id, path:path.fileSystem, rev:newFileRef.rev+1, project_name:project.name}, (err) -> return callback(err) if err? - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldFiles, newFiles}, callback addDocWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> # This method should never be called directly, except when importing a project @@ -257,28 +268,36 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) if err? callback null, doc, !existingDoc? - # FIXME: this method needs to be changed, currently it needs a lock around an upload to S3 because - # there is no way to know in advance if the file is a replace or an insert (at least you have to - # take the lock to find that out and do any operation in that lock). In the new scheme any upsert - # should always create the new file (outside the lock) in S3 first and then make the changes to the - # project in a lock, marking the old file as deleted if it exists. - upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (err, file, isNewFile)->)-> - ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> - return callback(error) if error? - return callback(new Error("Couldn't find folder")) if !folder? - existingFile = null - for fileRef in folder.fileRefs - if fileRef.name == fileName - existingFile = fileRef - break - if existingFile? - self.replaceFile.withoutLock project_id, existingFile._id, fsPath, linkedFileData, userId, (err) -> + upsertFile: wrapWithLock + beforeLock: (next) -> + (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback)-> + # create a new file + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> return callback(err) if err? - callback null, existingFile, !existingFile? - else - self.addFile.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (err, file) -> - return callback(err) if err? - callback null, file, !existingFile? + next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) + withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback = (err, file, isNewFile, existingFile)->)-> + ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> + return callback(error) if error? + return callback(new Error("Couldn't find folder")) if !folder? + existingFile = null + for fileRef in folder.fileRefs + if fileRef.name == fileName + existingFile = fileRef + break + if existingFile? + # this calls directly into the replaceFile main task (without the beforeLock part) + self.replaceFile.mainTask project_id, existingFile._id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) -> + return callback(err) if err? + callback null, newFileRef, !existingFile?, existingFile + else + # this calls directly into the addFile main task (without the beforeLock part) + self.addFile.mainTask project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) -> + return callback(err) if err? + callback null, newFileRef, !existingFile?, existingFile upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) -> docName = path.basename(elementPath) @@ -289,14 +308,26 @@ module.exports = ProjectEntityUpdateHandler = self = return callback(err) if err? callback null, doc, isNewDoc, newFolders, folder - upsertFileWithPath: wrapWithLock (project_id, elementPath, fsPath, linkedFileData, userId, callback) -> - fileName = path.basename(elementPath) - folderPath = path.dirname(elementPath) - self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> - return callback(err) if err? - self.upsertFile.withoutLock project_id, folder._id, fileName, fsPath, linkedFileData, userId, (err, file, isNewFile) -> + upsertFileWithPath: wrapWithLock + beforeLock: (next) -> + (project_id, elementPath, fsPath, linkedFileData, userId, callback)-> + fileName = path.basename(elementPath) + folderPath = path.dirname(elementPath) + # create a new file + fileRef = new File( + name: fileName + linkedFileData: linkedFileData + ) + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + return callback(err) if err? + next project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback + withLock: (project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) -> + self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> return callback(err) if err? - callback null, file, isNewFile, newFolders, folder + # this calls directly into the upsertFile main task (without the beforeLock part) + self.upsertFile.mainTask project_id, folder._id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, (err, newFile, isNewFile, existingFile) -> + return callback(err) if err? + callback null, newFile, isNewFile, existingFile, newFolders, folder deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)-> logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" @@ -395,7 +426,7 @@ module.exports = ProjectEntityUpdateHandler = self = unsetRootDocIfRequired (error) -> return callback(error) if error? - self._insertDeletedDocReference project._id, doc, (error) -> + ProjectEntityMongoUpdateHandler._insertDeletedDocReference project._id, doc, (error) -> return callback(error) if error? DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> return callback(error) if error? @@ -405,11 +436,12 @@ module.exports = ProjectEntityUpdateHandler = self = DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback _cleanUpFile: (project, file, path, userId, callback = (error) ->) -> - project_id = project._id.toString() - file_id = file._id.toString() - FileStoreHandler.deleteFile project_id, file_id, (error) -> + ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, (error) -> return callback(error) if error? + project_id = project._id.toString() changes = oldFiles: [ {file, path} ] + # we are now keeping a copy of every file versio so we no longer delete + # the file from the filestore DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> @@ -430,15 +462,3 @@ module.exports = ProjectEntityUpdateHandler = self = jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback async.series jobs, callback - - _insertDeletedDocReference: (project_id, doc, callback = (error) ->) -> - Project.update { - _id: project_id - }, { - $push: { - deletedDocs: { - _id: doc._id - name: doc.name - } - } - }, {}, callback diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index fb0bc0fd6b..00c2f1be52 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -12,6 +12,10 @@ ObjectId = Schema.ObjectId DeletedDocSchema = new Schema name: String +DeletedFileSchema = new Schema + name: String + deletedAt: {type: Date} + ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} @@ -30,6 +34,7 @@ ProjectSchema = new Schema description : {type:String, default:''} archived : { type: Boolean } deletedDocs : [DeletedDocSchema] + deletedFiles : [DeletedFileSchema] imageName : { type: String } track_changes : { type: Object } tokens : diff --git a/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee b/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee index 5da94856fc..b4742eefec 100644 --- a/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee @@ -186,10 +186,13 @@ describe "ProjectDuplicateNames", -> contentType: 'image/jpeg' , (err, res, body) => @body = body + # update the image id because we have replaced the file + @imageFile._id = @body.entity_id done() it "should succeed (overwriting the file)", -> expect(@body.success).to.equal true + # at this point the @imageFile._id has changed describe "for an existing folder", -> describe "trying to add a doc with the same name", -> diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index 51bd4a26c7..30fbccfb8d 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -56,7 +56,6 @@ describe "ProjectStructureChanges", -> describe "duplicating a project", -> before (done) -> MockDocUpdaterApi.clearProjectStructureUpdates() - console.log(example_project_id) @owner.request.post { uri: "/Project/#{example_project_id}/clone", json: @@ -219,11 +218,17 @@ describe "ProjectStructureChanges", -> if res.statusCode < 200 || res.statusCode >= 300 throw new Error("failed to upload file #{res.statusCode}") + example_file_id = JSON.parse(body).entity_id + {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) + expect(updates.length).to.equal(2) update = updates[0] expect(update.userId).to.equal(@owner._id) expect(update.pathname).to.equal("/1pixel.png") + #expect(update.url).to.be.a('string'); + update = updates[1] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/1pixel.png") expect(update.url).to.be.a('string'); expect(version).to.equal(@project_0.version + 1) @@ -591,10 +596,14 @@ describe "ProjectStructureChanges", -> throw new Error("failed to upload file #{res.statusCode}") {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id) - expect(updates.length).to.equal(1) + expect(updates.length).to.equal(2) update = updates[0] expect(update.userId).to.equal(@owner._id) expect(update.pathname).to.equal("/1pixel.png") + #expect(update.url).to.be.a('string'); + update = updates[1] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/1pixel.png") expect(update.url).to.be.a('string'); expect(version).to.equal(@project_0.version + 1) diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index 9e30ef3fd1..703a03d14f 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -20,6 +20,8 @@ describe "EditorController", -> @fsPath = "/folder/file.png" @linkedFileData = {provider: 'url'} + @newFile = _id: "new-file-id" + @folder_id = "123ksajdn" @folder = _id: @folder_id @folderName = "folder" @@ -107,7 +109,7 @@ describe "EditorController", -> describe 'upsertFile', -> beforeEach -> - @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @file, false) + @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @newFile, false, @file) @EditorController.upsertFile @project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @source, @user_id, @callback it 'upserts the file using the project entity handler', -> @@ -116,7 +118,7 @@ describe "EditorController", -> .should.equal true it 'returns the file', -> - @callback.calledWith(null, @file).should.equal true + @callback.calledWith(null, @newFile).should.equal true describe 'file does not exist', -> beforeEach -> @@ -171,7 +173,7 @@ describe "EditorController", -> beforeEach -> @filePath = '/folder/file' - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, false, [], @folder) + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @newFile, false, @file, [], @folder) @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback it 'upserts the file using the project entity handler', -> @@ -181,7 +183,7 @@ describe "EditorController", -> describe 'file does not exist', -> beforeEach -> - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, [], @folder) + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, undefined, [], @folder) @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback it 'should send the update for the file out to users in the project', -> @@ -195,7 +197,7 @@ describe "EditorController", -> @folderA = { _id: 2, parentFolder_id: 1} @folderB = { _id: 3, parentFolder_id: 2} ] - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, folders, @folderB) + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, undefined, folders, @folderB) @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback it 'should send the update for each folder to users in the project', -> diff --git a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee index e05d5bc38a..57911bd7d8 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee @@ -93,33 +93,50 @@ describe 'ProjectEntityMongoUpdateHandler', -> .calledWith(@project, folder_id, @file, 'file', @callback) .should.equal true - describe 'replaceFile', -> + describe 'replaceFileWithNew', -> beforeEach -> @file = _id: file_id @path = mongo: 'file.png' - @linkedFileData = {provider: 'url'} + @newFile = _id: 'new-file-id' + @newFile.linkedFileData = @linkedFileData = {provider: 'url'} @ProjectLocator.findElement = sinon.stub().yields(null, @file, @path) @ProjectModel.update = sinon.stub().yields() - @subject.replaceFile project_id, file_id, @linkedFileData, @callback + @subject.replaceFileWithNew project_id, file_id, @newFile, @callback it 'gets the project', -> @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name: true}) .should.equal true - it 'finds the element', -> + it 'finds the existing element', -> @ProjectLocator.findElement .calledWith({ @project, element_id: file_id, type: 'file' }) .should.equal true - it 'increments the rev and sets the created_at', -> + it 'inserts a deletedFile reference for the old file', -> + @ProjectModel.update + .calledWith({ _id: project_id }, + { + $push: { + deletedFiles: { + _id: file_id + name: @file.name + linkedFileData: @file.linkedFileData + deletedAt: new Date() + } + } + } + ) + .should.equal true + + it 'increments the project version and sets the rev and created_at', -> @ProjectModel.update .calledWith( { _id: project_id }, { - '$inc': { 'file.png.rev': 1, 'version': 1 } - '$set': { 'file.png.created': new Date(), 'file.png.linkedFileData': @linkedFileData } + '$inc': { 'version': 1 } + '$set': { 'file.png._id': @newFile._id, 'file.png.created': new Date(), 'file.png.linkedFileData': @linkedFileData, 'file.png.rev': 1 } } {} ) @@ -596,3 +613,29 @@ describe 'ProjectEntityMongoUpdateHandler', -> folder = name: 'folder_name' @subject._checkValidMove @project, 'folder', folder, fileSystem: '/foo', @destFolder._id, (err) => expect(err).to.deep.equal new Errors.InvalidNameError("destination folder is a child folder of me") + + describe "_insertDeletedDocReference", -> + beforeEach -> + @doc = + _id: ObjectId() + name: "test.tex" + @callback = sinon.stub() + @ProjectModel.update = sinon.stub().yields() + @subject._insertDeletedDocReference project_id, @doc, @callback + + it "should insert the doc into deletedDocs", -> + @ProjectModel.update + .calledWith({ + _id: project_id + }, { + $push: { + deletedDocs: { + _id: @doc._id + name: @doc.name + } + } + }) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index 766e360840..09d0d274fb 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -14,6 +14,7 @@ describe 'ProjectEntityUpdateHandler', -> file_id = "4eecaffcbffa66588e000009" folder_id = "4eecaffcbffa66588e000008" rootFolderId = "4eecaffcbffa66588e000007" + new_file_id = "4eecaffcbffa66588e000099" userId = 1234 beforeEach -> @@ -31,7 +32,11 @@ describe 'ProjectEntityUpdateHandler', -> @FileModel = class File constructor:(options)-> {@name} = options - @._id = file_id + # use a new id for replacement files + if @name is 'dummy-upload-filename' + @._id = new_file_id + else + @._id = file_id @rev = 0 @docName = "doc-name" @@ -336,21 +341,26 @@ describe 'ProjectEntityUpdateHandler', -> describe 'replaceFile', -> beforeEach -> - @newFile = _id: file_id, rev: 0 + # replacement file now creates a new file object + @newFileUrl = "new-file-url" + @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl) + + @newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0 + @oldFile = _id: file_id @path = "/path/to/file" @project = _id: project_id, name: 'some project' - @ProjectEntityMongoUpdateHandler.replaceFile = sinon.stub().yields(null, @newFile, @project, fileSystem: @path) - + @ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields() + @ProjectEntityMongoUpdateHandler.replaceFileWithNew = sinon.stub().yields(null, @oldFile, @project, fileSystem: @path) @ProjectEntityUpdateHandler.replaceFile project_id, file_id, @fileSystemPath, @linkedFileData, userId, @callback it 'uploads a new version of the file', -> @FileStoreHandler.uploadFileFromDisk - .calledWith(project_id, file_id, @fileSystemPath) + .calledWith(project_id, new_file_id, @fileSystemPath) .should.equal true it 'replaces the file in mongo', -> - @ProjectEntityMongoUpdateHandler.replaceFile - .calledWith(project_id, file_id, @linkedFileData) + @ProjectEntityMongoUpdateHandler.replaceFileWithNew + .calledWith(project_id, file_id, @newFile) .should.equal true it 'notifies the tpds', -> @@ -358,20 +368,24 @@ describe 'ProjectEntityUpdateHandler', -> .calledWith({ project_id: project_id project_name: @project.name - file_id: file_id + file_id: new_file_id rev: @newFile.rev + 1 path: @path }) .should.equal true it 'updates the project structure in the doc updater', -> + oldFiles = [ + file: @oldFile + path: @path + ] newFiles = [ file: @newFile path: @path - url: @fileUrl + url: @newFileUrl ] @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {newFiles}) + .calledWith(project_id, userId, {oldFiles, newFiles}) .should.equal true describe 'addDocWithoutUpdatingHistory', -> @@ -530,12 +544,12 @@ describe 'ProjectEntityUpdateHandler', -> @existingFile = _id: file_id, name: @fileName @folder = _id: folder_id, fileRefs: [@existingFile] @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.replaceFile = withoutLock: sinon.stub().yields(null, @newFile) + @ProjectEntityUpdateHandler.replaceFile = mainTask: sinon.stub().yields(null, @newFile) @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback it 'replaces the file', -> - @ProjectEntityUpdateHandler.replaceFile.withoutLock + @ProjectEntityUpdateHandler.replaceFile.mainTask .calledWith(project_id, file_id, @fileSystemPath, @linkedFileData, userId) .should.equal true @@ -547,7 +561,7 @@ describe 'ProjectEntityUpdateHandler', -> @folder = _id: folder_id, fileRefs: [] @newFile = _id: file_id @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.addFile = withoutLock: sinon.stub().yields(null, @newFile) + @ProjectEntityUpdateHandler.addFile = mainTask: sinon.stub().yields(null, @newFile) @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback @@ -557,7 +571,7 @@ describe 'ProjectEntityUpdateHandler', -> .should.equal true it 'adds the file', -> - @ProjectEntityUpdateHandler.addFile.withoutLock + @ProjectEntityUpdateHandler.addFile.mainTask .calledWith(project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId) .should.equal true @@ -603,7 +617,7 @@ describe 'ProjectEntityUpdateHandler', -> @ProjectEntityUpdateHandler.mkdirp = withoutLock: sinon.stub().yields(null, @newFolders, @folder) @ProjectEntityUpdateHandler.upsertFile = - withoutLock: sinon.stub().yields(null, @file, @isNewFile) + mainTask: sinon.stub().yields(null, @file, @isNewFile) @ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback @@ -613,13 +627,13 @@ describe 'ProjectEntityUpdateHandler', -> .should.equal true it 'upserts the file', -> - @ProjectEntityUpdateHandler.upsertFile.withoutLock + @ProjectEntityUpdateHandler.upsertFile.mainTask .calledWith(project_id, @folder._id, 'file.png', @fileSystemPath, @linkedFileData, userId) .should.equal true it 'calls the callback', -> @callback - .calledWith(null, @file, @isNewFile, @newFolders, @folder) + .calledWith(null, @file, @isNewFile, undefined, @newFolders, @folder) .should.equal true describe 'deleteEntity', -> @@ -838,6 +852,7 @@ describe 'ProjectEntityUpdateHandler', -> @FileStoreHandler.deleteFile = sinon.stub().yields() @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + @ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields() describe "a file", -> beforeEach (done) -> @@ -845,8 +860,13 @@ describe 'ProjectEntityUpdateHandler', -> @entity = _id: @entity_id @ProjectEntityUpdateHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done - it "should delete the file from FileStoreHandler", -> - @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true + it "should insert the file into the deletedFiles array", -> + @ProjectEntityMongoUpdateHandler._insertDeletedFileReference + .calledWith(@project._id, @entity) + .should.equal true + + it "should not delete the file from FileStoreHandler", -> + @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal false it "should not attempt to delete from the document updater", -> @DocumentUpdaterHandler.deleteDoc.called.should.equal false @@ -911,7 +931,7 @@ describe 'ProjectEntityUpdateHandler', -> name: "test.tex" @path = "/path/to/doc" @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() - @ProjectEntityUpdateHandler._insertDeletedDocReference = sinon.stub().yields() + @ProjectEntityMongoUpdateHandler._insertDeletedDocReference = sinon.stub().yields() @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() @DocstoreManager.deleteDoc = sinon.stub().yields() @callback = sinon.stub() @@ -931,7 +951,7 @@ describe 'ProjectEntityUpdateHandler', -> .calledWith(project_id, @doc._id.toString()) it "should insert the doc into the deletedDocs array", -> - @ProjectEntityUpdateHandler._insertDeletedDocReference + @ProjectEntityMongoUpdateHandler._insertDeletedDocReference .calledWith(@project._id, @doc) .should.equal true @@ -960,28 +980,4 @@ describe 'ProjectEntityUpdateHandler', -> it "should call the callback", -> @callback.called.should.equal true - describe "_insertDeletedDocReference", -> - beforeEach -> - @doc = - _id: ObjectId() - name: "test.tex" - @callback = sinon.stub() - @ProjectModel.update = sinon.stub().yields() - @ProjectEntityUpdateHandler._insertDeletedDocReference project_id, @doc, @callback - - it "should insert the doc into deletedDocs", -> - @ProjectModel.update - .calledWith({ - _id: project_id - }, { - $push: { - deletedDocs: { - _id: @doc._id - name: @doc.name - } - } - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true + From 35cc5bf19c1224546539eccb2609debb34d392ef Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 10 Apr 2018 10:38:40 +0100 Subject: [PATCH 17/47] Add defensive check for req.query, fixing tests --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index e95d208900..77aabd8524 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -301,7 +301,7 @@ module.exports = ProjectController = themes: THEME_LIST maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display - showRichText: req.query.rt == 'true' + showRichText: req.query?.rt == 'true' timer.done() _buildProjectList: (allProjects, v1Projects = [])-> From a250004036becb6ccb1d186bf127351d473afdef Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 29 Mar 2018 16:15:53 +0100 Subject: [PATCH 18/47] initial publish modal setup --- services/web/app/coffee/infrastructure/Features.coffee | 2 ++ services/web/app/views/project/editor.pug | 4 +++- services/web/app/views/project/editor/header.pug | 3 +++ services/web/app/views/project/editor/publish.pug | 9 +++++++++ services/web/package.json | 1 + services/web/webpack.config.js | 10 +++++++++- 6 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 services/web/app/views/project/editor/publish.pug diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 519cfda6da..be7cd21d89 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -14,6 +14,8 @@ module.exports = Features = return Settings.enableGithubSync when 'v1-return-message' return Settings.accountMerge? and Settings.overleaf? + when 'publish-modal' + return Settings.showPublishModal when 'custom-togglers' return Settings.overleaf? else diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 214ae202fe..d004101546 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -55,6 +55,7 @@ block content include ./editor/header include ./editor/share + include ./editor/publish #ide-body( ng-cloak, @@ -136,7 +137,8 @@ block requirejs "fineuploader": "libs/#{lib('fineuploader')}", "ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", - !{moduleIncludes("editor:script", locals)} + !{moduleIncludes("editor:script", locals)}, + !{moduleIncludes("publish:script", locals)} }, "waitSeconds": 0, "shim": { diff --git a/services/web/app/views/project/editor/header.pug b/services/web/app/views/project/editor/header.pug index c80c4d7b3d..03b56d9e20 100644 --- a/services/web/app/views/project/editor/header.pug +++ b/services/web/app/views/project/editor/header.pug @@ -107,6 +107,9 @@ header.toolbar.toolbar-header.toolbar-with-labels( ) i.fa.fa-fw.fa-group p.toolbar-label #{translate("share")} + + != moduleIncludes('publish:button', locals) + a.btn.btn-full-height( href, ng-click="toggleHistory();", diff --git a/services/web/app/views/project/editor/publish.pug b/services/web/app/views/project/editor/publish.pug new file mode 100644 index 0000000000..8ef74f3d67 --- /dev/null +++ b/services/web/app/views/project/editor/publish.pug @@ -0,0 +1,9 @@ +script(type='text/ng-template', id='publishProjectModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + ) × + h3 Publish} + .modal-body.modal-body-publish diff --git a/services/web/package.json b/services/web/package.json index ae9073fcfa..570ef55a2d 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -72,6 +72,7 @@ "passport-oauth2-refresh": "^1.0.0", "passport-saml": "^0.15.0", "pug": "^2.0.0-beta6", + "react": "^15.4.2", "redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.4", "request": "^2.69.0", "requestretry": "^1.13.0", diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 50dc47efb4..93107ada1d 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -59,9 +59,17 @@ module.exports = { cacheDirectory: true } }] + }, { + // Export React as a global variable from the bundle + test: require.resolve('react'), + use: [{ + loader: 'expose-loader', + options: 'React' + }] }] }, // TODO // plugins: {} -} \ No newline at end of file +} + From 39d25fdff9984b0adbede13ccf6a86152f8d6cd0 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 29 Mar 2018 17:48:05 +0100 Subject: [PATCH 19/47] webpack compiling for publish menu --- services/web/app/views/project/editor.pug | 2 +- services/web/app/views/project/editor/publish.pug | 2 +- services/web/package.json | 3 +++ services/web/webpack.config.js | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index d004101546..5e3598909d 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -137,7 +137,7 @@ block requirejs "fineuploader": "libs/#{lib('fineuploader')}", "ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", - !{moduleIncludes("editor:script", locals)}, + !{moduleIncludes("editor:script", locals)} !{moduleIncludes("publish:script", locals)} }, "waitSeconds": 0, diff --git a/services/web/app/views/project/editor/publish.pug b/services/web/app/views/project/editor/publish.pug index 8ef74f3d67..a4b9247b93 100644 --- a/services/web/app/views/project/editor/publish.pug +++ b/services/web/app/views/project/editor/publish.pug @@ -5,5 +5,5 @@ script(type='text/ng-template', id='publishProjectModalTemplate') data-dismiss="modal" ng-click="cancel()" ) × - h3 Publish} + h3 Publish .modal-body.modal-body-publish diff --git a/services/web/package.json b/services/web/package.json index 570ef55a2d..f86a0d57ea 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -38,10 +38,12 @@ "cookie-parser": "1.3.5", "csurf": "^1.8.3", "dateformat": "1.0.4-1.2.3", + "expose-loader": "^0.7.3", "express": "4.13.0", "express-http-proxy": "^1.1.0", "express-session": "^1.14.2", "fs-extra": "^4.0.2", + "fuse.js": "^3.0.0", "heapdump": "^0.3.7", "helmet": "^3.8.1", "http-proxy": "^1.8.1", @@ -96,6 +98,7 @@ "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.16.0", "bunyan": "0.22.1", "chai": "3.5.0", "chai-spies": "", diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 93107ada1d..a5b6501e66 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -52,6 +52,7 @@ module.exports = { loader: 'babel-loader', options: { presets: [ + 'react', ['env', { modules: false }] ], // Configure babel-loader to cache compiled output so that subsequent From 6c3482f2a1e841b9022702b55f9e949e5781e9a3 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Tue, 10 Apr 2018 10:01:10 +0100 Subject: [PATCH 20/47] add reactDOM, fix publish menu configs --- services/web/app/views/project/editor.pug | 2 +- .../web/app/views/project/editor/publish.pug | 9 -- services/web/npm-shrinkwrap.json | 134 ++++++++++++++++-- services/web/package.json | 2 +- services/web/webpack.config.js | 9 +- 5 files changed, 129 insertions(+), 27 deletions(-) delete mode 100644 services/web/app/views/project/editor/publish.pug diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 5e3598909d..da13deaa1f 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -55,7 +55,7 @@ block content include ./editor/header include ./editor/share - include ./editor/publish + != moduleIncludes("publish:body", locals) #ide-body( ng-cloak, diff --git a/services/web/app/views/project/editor/publish.pug b/services/web/app/views/project/editor/publish.pug deleted file mode 100644 index a4b9247b93..0000000000 --- a/services/web/app/views/project/editor/publish.pug +++ /dev/null @@ -1,9 +0,0 @@ -script(type='text/ng-template', id='publishProjectModalTemplate') - .modal-header - button.close( - type="button" - data-dismiss="modal" - ng-click="cancel()" - ) × - h3 Publish - .modal-body.modal-body-publish diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index 8f09d13777..b753fb0ffb 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -533,6 +533,12 @@ "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", "dev": true }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "from": "babel-helper-builder-react-jsx@>=6.24.1 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", + "dev": true + }, "babel-helper-call-delegate": { "version": "6.24.1", "from": "babel-helper-call-delegate@>=6.24.1 <7.0.0", @@ -629,6 +635,18 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", "dev": true }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "from": "babel-plugin-syntax-flow@>=6.18.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "dev": true + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "from": "babel-plugin-syntax-jsx@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "dev": true + }, "babel-plugin-syntax-trailing-function-commas": { "version": "6.22.0", "from": "babel-plugin-syntax-trailing-function-commas@>=6.22.0 <7.0.0", @@ -779,6 +797,36 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", "dev": true }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "from": "babel-plugin-transform-flow-strip-types@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "dev": true + }, + "babel-plugin-transform-react-display-name": { + "version": "6.25.0", + "from": "babel-plugin-transform-react-display-name@>=6.23.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", + "dev": true + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "from": "babel-plugin-transform-react-jsx@>=6.24.1 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "dev": true + }, + "babel-plugin-transform-react-jsx-self": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-jsx-self@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", + "dev": true + }, + "babel-plugin-transform-react-jsx-source": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-jsx-source@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", + "dev": true + }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "from": "babel-plugin-transform-regenerator@>=6.22.0 <7.0.0", @@ -805,6 +853,18 @@ } } }, + "babel-preset-flow": { + "version": "6.23.0", + "from": "babel-preset-flow@>=6.23.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", + "dev": true + }, + "babel-preset-react": { + "version": "6.24.1", + "from": "babel-preset-react@>=6.16.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", + "dev": true + }, "babel-register": { "version": "6.26.0", "from": "babel-register@>=6.26.0 <7.0.0", @@ -2031,6 +2091,11 @@ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", "dev": true }, + "create-react-class": { + "version": "15.6.3", + "from": "create-react-class@>=15.6.0 <16.0.0", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz" + }, "cross-spawn": { "version": "5.1.0", "from": "cross-spawn@>=5.0.1 <6.0.0", @@ -3429,6 +3494,23 @@ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", "dev": true }, + "fbjs": { + "version": "0.8.16", + "from": "fbjs@>=0.8.9 <0.9.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + }, + "promise": { + "version": "7.3.1", + "from": "promise@>=7.1.1 <8.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" + } + } + }, "fd-slicer": { "version": "1.0.1", "from": "fd-slicer@>=1.0.1 <1.1.0", @@ -3744,6 +3826,11 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "dev": true }, + "fuse.js": { + "version": "3.2.0", + "from": "fuse.js@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.0.tgz" + }, "gauge": { "version": "2.7.4", "from": "gauge@>=2.7.3 <2.8.0", @@ -5307,8 +5394,7 @@ "is-stream": { "version": "1.1.0", "from": "is-stream@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" }, "is-symbol": { "version": "1.0.1", @@ -5355,6 +5441,11 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "dev": true }, + "isomorphic-fetch": { + "version": "2.2.1", + "from": "isomorphic-fetch@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz" + }, "isstream": { "version": "0.1.2", "from": "isstream@>=0.1.2 <0.2.0", @@ -5391,8 +5482,7 @@ "js-tokens": { "version": "3.0.2", "from": "js-tokens@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz" }, "js-yaml": { "version": "2.0.5", @@ -6858,8 +6948,7 @@ "loose-envify": { "version": "1.3.1", "from": "loose-envify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" }, "loud-rejection": { "version": "1.6.0", @@ -7620,6 +7709,11 @@ "from": "nocache@2.0.0", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz" }, + "node-fetch": { + "version": "1.7.3", + "from": "node-fetch@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz" + }, "node-forge": { "version": "0.2.24", "from": "node-forge@0.2.24", @@ -8663,6 +8757,11 @@ "resolved": "https://registry.npmjs.org/prompt/-/prompt-0.2.14.tgz", "dev": true }, + "prop-types": { + "version": "15.6.1", + "from": "prop-types@>=15.5.10 <16.0.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz" + }, "proxy-addr": { "version": "1.0.10", "from": "proxy-addr@>=1.0.8 <1.1.0", @@ -8967,6 +9066,16 @@ } } }, + "react": { + "version": "15.6.2", + "from": "react@>=15.4.2 <16.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz" + }, + "react-dom": { + "version": "15.6.2", + "from": "react-dom@>=15.4.2 <16.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz" + }, "read": { "version": "1.0.7", "from": "read@>=1.0.0 <1.1.0", @@ -9676,8 +9785,7 @@ "setimmediate": { "version": "1.0.5", "from": "setimmediate@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" }, "setprototypeof": { "version": "1.0.3", @@ -10945,6 +11053,11 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "dev": true }, + "ua-parser-js": { + "version": "0.7.17", + "from": "ua-parser-js@>=0.7.9 <0.8.0", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz" + }, "uglify-js": { "version": "2.4.24", "from": "uglify-js@>=2.4.0 <2.5.0", @@ -12061,6 +12174,11 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", "dev": true }, + "whatwg-fetch": { + "version": "2.0.4", + "from": "whatwg-fetch@>=0.10.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz" + }, "when": { "version": "3.7.8", "from": "when@>=3.7.7 <4.0.0", diff --git a/services/web/package.json b/services/web/package.json index f86a0d57ea..b6a4d80d35 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -38,7 +38,6 @@ "cookie-parser": "1.3.5", "csurf": "^1.8.3", "dateformat": "1.0.4-1.2.3", - "expose-loader": "^0.7.3", "express": "4.13.0", "express-http-proxy": "^1.1.0", "express-session": "^1.14.2", @@ -75,6 +74,7 @@ "passport-saml": "^0.15.0", "pug": "^2.0.0-beta6", "react": "^15.4.2", + "react-dom": "^15.4.2", "redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.4", "request": "^2.69.0", "requestretry": "^1.13.0", diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index a5b6501e66..da0dc8257e 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -52,7 +52,7 @@ module.exports = { loader: 'babel-loader', options: { presets: [ - 'react', + 'react', ['env', { modules: false }] ], // Configure babel-loader to cache compiled output so that subsequent @@ -60,13 +60,6 @@ module.exports = { cacheDirectory: true } }] - }, { - // Export React as a global variable from the bundle - test: require.resolve('react'), - use: [{ - loader: 'expose-loader', - options: 'React' - }] }] }, From 60a28f71735557ae19ab351569bcfebe433022f2 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Apr 2018 12:00:36 +0100 Subject: [PATCH 21/47] Fix hashed file path for rich text --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index ee0933c612..0855a35c74 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -41,7 +41,7 @@ pathList = [ "#{jsPath}ide.js" "#{jsPath}main.js" "#{jsPath}libraries.js" - "#{jsPath}es/richText.js" + "#{jsPath}es/rich-text.js" "/stylesheets/style.css" "/stylesheets/ol-style.css" ] From f44ddcb928de798e87f5e94c3b1fd32d5ba6c58f Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 12:39:46 -0500 Subject: [PATCH 22/47] Remove "plans-1610" experiment There is 1 place that the variants appear still, `/web/public/coffee/main/subscription-dashboard.coffee`. This should remain for users that bought these plans. --- .../web/app/views/subscriptions/plans.pug | 14 +- .../public/coffee/main/account-upgrade.coffee | 8 +- services/web/public/coffee/main/plans.coffee | 166 ------------------ .../coffee/main/subscription-dashboard.coffee | 2 - 4 files changed, 5 insertions(+), 185 deletions(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index bafb855ff7..4d97ebd947 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -90,10 +90,8 @@ block content | {{plans[currencyCode]['collaborator']['annual']}} span.small /yr ul.list-unstyled - li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})} + li + strong #{translate("collabs_per_proj", {collabcount:10})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -157,9 +155,7 @@ block content span.small /mo ul.list-unstyled li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} + strong #{translate("collabs_per_proj", {collabcount:6})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -180,9 +176,7 @@ block content span.small /yr ul.list-unstyled li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} + strong #{translate("collabs_per_proj", {collabcount:6})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 0cabc224a0..5013007a90 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -33,10 +33,4 @@ define [ w.location = url - if $scope.shouldABTestPlans - sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> - if chosenVariation in ['heron', 'ibis'] - plan = "collaborator_#{chosenVariation}" - go() - else - go() + go() diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index 060517a9d5..b74a4d1da9 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -11,164 +11,6 @@ define [ return { currencyCode:currencyCode - heron: - USD: - student: - monthly: "$6" - annual: "$60" - collaborator: - monthly: "$12" - annual: "$144" - EUR: - student: - monthly: "€5" - annual: "€50" - collaborator: - monthly: "€11" - annual: "€132" - GBP: - student: - monthly: "£5" - annual: "£50" - collaborator: - monthly: "£10" - annual: "£120" - SEK: - student: - monthly: "45 kr" - annual: "450 kr" - collaborator: - monthly: "90 kr" - annual: "1080 kr" - CAD: - student: - monthly: "$7" - annual: "$70" - collaborator: - monthly: "$14" - annual: "$168" - NOK: - student: - monthly: "45 kr" - annual: "450 kr" - collaborator: - monthly: "90 kr" - annual: "1080 kr" - DKK: - student: - monthly: "40 kr" - annual: "400 kr" - collaborator: - monthly: "70 kr" - annual: "840 kr" - AUD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$15" - annual: "$180" - NZD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$15" - annual: "$180" - CHF: - student: - monthly: "Fr 6" - annual: "Fr 60" - collaborator: - monthly: "Fr 12" - annual: "Fr 144" - SGD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$16" - annual: "$192" - - ibis: - USD: - student: - monthly: "$10" - annual: "$100" - collaborator: - monthly: "$18" - annual: "$216" - EUR: - student: - monthly: "€9" - annual: "€90" - collaborator: - monthly: "€17" - annual: "€204" - GBP: - student: - monthly: "£7" - annual: "£70" - collaborator: - monthly: "£14" - annual: "£168" - SEK: - student: - monthly: "75 kr" - annual: "750 kr" - collaborator: - monthly: "140 kr" - annual: "1680 kr" - CAD: - student: - monthly: "$12" - annual: "$120" - collaborator: - monthly: "$22" - annual: "$264" - NOK: - student: - monthly: "75 kr" - annual: "750 kr" - collaborator: - monthly: "140 kr" - annual: "1680 kr" - DKK: - student: - monthly: "68 kr" - annual: "680 kr" - collaborator: - monthly: "110 kr" - annual: "1320 kr" - AUD: - student: - monthly: "$13" - annual: "$130" - collaborator: - monthly: "$22" - annual: "$264" - NZD: - student: - monthly: "$14" - annual: "$140" - collaborator: - monthly: "$22" - annual: "$264" - CHF: - student: - monthly: "Fr 10" - annual: "Fr 100" - collaborator: - monthly: "Fr 18" - annual: "Fr 216" - SGD: - student: - monthly: "$14" - annual: "$140" - collaborator: - monthly: "$25" - annual: "$300" - plans: USD: symbol: "$" @@ -312,14 +154,6 @@ define [ $scope.shouldABTestPlans = window.shouldABTestPlans if $scope.shouldABTestPlans - sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> - $scope.plansVariant = chosenVariation - event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation} - if chosenVariation in ['heron', 'ibis'] - # overwrite student plans with alternative - for currency, _v of $scope.plans - $scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student'] - $scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator'] $scope.showPlans = true else $scope.showPlans = true diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee index c971b9712a..28d5af013b 100644 --- a/services/web/public/coffee/main/subscription-dashboard.coffee +++ b/services/web/public/coffee/main/subscription-dashboard.coffee @@ -3,8 +3,6 @@ define [ ], (App)-> App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) -> - sixpack.convert 'plans-1610', () -> - SUBSCRIPTION_URL = "/user/subscription/update" From 5683f48d97344ff03a95ee5d63e2e35d674a8e30 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 12:54:34 -0500 Subject: [PATCH 23/47] Fix conditional --- services/web/app/views/subscriptions/plans.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 4d97ebd947..7c40c38a52 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -76,7 +76,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + style=(getLoggedInUserId() === null ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 .card.card-highlighted @@ -142,7 +142,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + style=(getLoggedInUserId() === null ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 From 55cb5b3c7045190779ea4dd969cf151e2c487fd1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 14:10:36 -0500 Subject: [PATCH 24/47] Remove "teaser-history" experiment --- services/web/app/views/project/editor/history.pug | 1 - services/web/public/coffee/main/account-upgrade.coffee | 4 ---- 2 files changed, 5 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 7b4436af5d..48bef7adfb 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -33,7 +33,6 @@ div#history(ng-show="ui.view == 'history'") href ng-class="buttonClass" ng-click="startFreeTrial('history')" - sixpack-convert="teaser-history" ) #{translate("start_free_trial")} .message(ng-show="project.owner._id != user.id") diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 5013007a90..28207ce9ec 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -24,10 +24,6 @@ define [ sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) -> event_tracking.sendMB "subscription-start-trial", { source, plan, variant } - when "history" - sixpack.participate 'teaser-history', ['default', 'focused'], (variant) -> - event_tracking.sendMB "subscription-start-trial", { source, plan, variant } - else event_tracking.sendMB "subscription-start-trial", { source, plan } From 2301b83f227f58298de1a894a748cf7fb614d3ba Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 14:11:00 -0500 Subject: [PATCH 25/47] Remove "teaser-dropbox-text" experiment --- services/web/public/coffee/main/account-upgrade.coffee | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 28207ce9ec..638371e45e 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -19,13 +19,7 @@ define [ url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true - switch source - when "dropbox" - sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) -> - event_tracking.sendMB "subscription-start-trial", { source, plan, variant } - - else - event_tracking.sendMB "subscription-start-trial", { source, plan } + event_tracking.sendMB "subscription-start-trial", { source, plan } w.location = url From be5f95ba6936cfb9c2d8f06f6fc091a483b48454 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 14:22:20 -0500 Subject: [PATCH 26/47] Remove "history-discount" experiment --- .../ide/history/controllers/HistoryListController.coffee | 9 --------- 1 file changed, 9 deletions(-) diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index c736014c8b..f16cace816 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -3,15 +3,6 @@ define [ "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)-> - $scope.$watch "ui.view", -> - if $scope.ui.view == "history" - if $scope.project?.features?.versioning - $scope.versioningPopupType = "default" - else if $scope.ui.view == "history" - sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> - $scope.versioningPopupType = chosenVariation - App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> $scope.hoveringOverListSelectors = false From f9a56f7b37a726aa6f56e32cee50854c4e98a777 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 9 Apr 2018 14:33:38 -0500 Subject: [PATCH 27/47] Remove unknown experiments --- services/web/app/views/project/editor/pdf.pug | 1 - services/web/app/views/project/editor/share.pug | 1 - services/web/app/views/project/list/side-bar.pug | 7 ++----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug index a87b765cf2..a874a0b88b 100644 --- a/services/web/app/views/project/editor/pdf.pug +++ b/services/web/app/views/project/editor/pdf.pug @@ -402,7 +402,6 @@ div.full-size.pdf(ng-controller="PdfController") a.btn.btn-success.row-spaced-small( href ng-class="buttonClass" - sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index b63038cf86..d2aa1cc173 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -173,7 +173,6 @@ script(type='text/ng-template', id='shareProjectModalTemplate') a.btn.btn-success( href ng-class="buttonClass" - sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('projectMembers')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index c4186a01d5..c695dfd30c 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -10,19 +10,16 @@ li a( href, - sixpack-convert="first_sign_up", ng-click="openCreateProjectModal()" ) #{translate("blank_project")} li a( href, - sixpack-convert="first_sign_up", ng-click="openCreateProjectModal('example')" ) #{translate("example_project")} li a( href, - sixpack-convert="first_sign_up", ng-click="openUploadProjectModal()" ) #{translate("upload_project")} != moduleIncludes("newProjectMenu", locals) @@ -31,7 +28,7 @@ li.dropdown-header #{translate("templates")} each item in templates li - a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} + a.menu-indent(href=item.url) #{translate(item.name)} .row-spaced(ng-if="projects.length > 0", ng-cloak) ul.list-unstyled.folders-menu( @@ -133,7 +130,7 @@ hr p.small #{translate("on_free_sl")} p - a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} + a(href="/user/subscription/plans").btn.btn-primary #{translate("upgrade")} p.small.text-centered | #{translate("or_unlock_features_bonus")} a(href="/user/bonus") #{translate("sharing_sl")} . From 1b8f4bff2f2f8e14a33935530d50dbe88ed1c086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= Date: Thu, 12 Apr 2018 11:45:48 +0100 Subject: [PATCH 28/47] Ensure dump folder exists before running LinkedFiles tests The folder is also created in another test, so it will work the second time the tests are executed, but it fails the first time. --- .../web/test/acceptance/coffee/LinkedFilesTests.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 77af535eb5..9ca7ecae42 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -1,7 +1,9 @@ async = require "async" expect = require("chai").expect _ = require 'underscore' +mkdirp = require "mkdirp" +Settings = require "settings-sharelatex" MockFileStoreApi = require './helpers/MockFileStoreApi' request = require "./helpers/request" User = require "./helpers/User" @@ -21,8 +23,9 @@ describe "LinkedFiles", -> before (done) -> LinkedUrlProxy.listen 6543, (error) => return done(error) if error? - @owner = new User() - @owner.login done + @owner = new User() + @owner.login -> + mkdirp Settings.path.dumpFolder, done describe "creating a URL based linked file", -> before (done) -> @@ -181,4 +184,4 @@ describe "LinkedFiles", -> done() # TODO: Add test for asking for host that return ENOTFOUND - # (This will probably end up handled by the proxy) \ No newline at end of file + # (This will probably end up handled by the proxy) From 35d7c96d04a1b9f4ee519a8bf873095e166119f0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Apr 2018 16:41:49 +0100 Subject: [PATCH 29/47] Control the visibility of the custom toggler when panes are hidden externally. --- services/web/public/coffee/ide/directives/layout.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/web/public/coffee/ide/directives/layout.coffee b/services/web/public/coffee/ide/directives/layout.coffee index 3c8f2900f1..b4e2e7f83d 100644 --- a/services/web/public/coffee/ide/directives/layout.coffee +++ b/services/web/public/coffee/ide/directives/layout.coffee @@ -119,6 +119,7 @@ define [ customTogglerScope = scope.$new() customTogglerScope.isOpen = true + customTogglerScope.isVisible = true if state[customTogglerPane]?.initClosed == true customTogglerScope.isOpen = false @@ -132,6 +133,7 @@ define [ repositionCustomToggler() customTogglerEl = $compile(" + customTogglerEl.scope().isOpen = !value + customTogglerEl.scope().isVisible = !value + post: (scope, element, attrs) -> name = attrs.layout From 8d2c3ca6a19b189ea015e621e2829ba5615d1322 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 12 Apr 2018 11:53:31 -0500 Subject: [PATCH 30/47] Remove undefined controller --- services/web/app/views/project/editor/history.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 48bef7adfb..2fc6b02ae8 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -1,5 +1,5 @@ div#history(ng-show="ui.view == 'history'") - span(ng-controller="HistoryPremiumPopup") + span .upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'") .message(ng-if="project.owner._id == user.id") p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})} From 5290c29ce10f7c36934eac79aafaf78c33ebeaa1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 12 Apr 2018 13:25:42 -0500 Subject: [PATCH 31/47] Revert "Merge pull request #480 from sharelatex/jel-remove-old-exp" This reverts commit c5919be6d2e880ac4d3f5820c38211b1c88812ff, reversing changes made to 3c8772cf020738c767b6f7a956bd002b47988932. --- .../web/app/views/project/editor/history.pug | 1 + services/web/app/views/project/editor/pdf.pug | 1 + .../web/app/views/project/editor/share.pug | 1 + .../web/app/views/project/list/side-bar.pug | 7 +- .../web/app/views/subscriptions/plans.pug | 18 +- .../controllers/HistoryListController.coffee | 9 + .../public/coffee/main/account-upgrade.coffee | 20 ++- services/web/public/coffee/main/plans.coffee | 166 ++++++++++++++++++ .../coffee/main/subscription-dashboard.coffee | 2 + 9 files changed, 215 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 48bef7adfb..7b4436af5d 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -33,6 +33,7 @@ div#history(ng-show="ui.view == 'history'") href ng-class="buttonClass" ng-click="startFreeTrial('history')" + sixpack-convert="teaser-history" ) #{translate("start_free_trial")} .message(ng-show="project.owner._id != user.id") diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug index a874a0b88b..a87b765cf2 100644 --- a/services/web/app/views/project/editor/pdf.pug +++ b/services/web/app/views/project/editor/pdf.pug @@ -402,6 +402,7 @@ div.full-size.pdf(ng-controller="PdfController") a.btn.btn-success.row-spaced-small( href ng-class="buttonClass" + sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index d2aa1cc173..b63038cf86 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -173,6 +173,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') a.btn.btn-success( href ng-class="buttonClass" + sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('projectMembers')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index c695dfd30c..c4186a01d5 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -10,16 +10,19 @@ li a( href, + sixpack-convert="first_sign_up", ng-click="openCreateProjectModal()" ) #{translate("blank_project")} li a( href, + sixpack-convert="first_sign_up", ng-click="openCreateProjectModal('example')" ) #{translate("example_project")} li a( href, + sixpack-convert="first_sign_up", ng-click="openUploadProjectModal()" ) #{translate("upload_project")} != moduleIncludes("newProjectMenu", locals) @@ -28,7 +31,7 @@ li.dropdown-header #{translate("templates")} each item in templates li - a.menu-indent(href=item.url) #{translate(item.name)} + a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} .row-spaced(ng-if="projects.length > 0", ng-cloak) ul.list-unstyled.folders-menu( @@ -130,7 +133,7 @@ hr p.small #{translate("on_free_sl")} p - a(href="/user/subscription/plans").btn.btn-primary #{translate("upgrade")} + a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} p.small.text-centered | #{translate("or_unlock_features_bonus")} a(href="/user/bonus") #{translate("sharing_sl")} . diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 7c40c38a52..bafb855ff7 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -76,7 +76,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === null ? "" : "visibility: hidden") + style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 .card.card-highlighted @@ -90,8 +90,10 @@ block content | {{plans[currencyCode]['collaborator']['annual']}} span.small /yr ul.list-unstyled - li - strong #{translate("collabs_per_proj", {collabcount:10})} + li + strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})} + strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})} + strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -142,7 +144,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === null ? "" : "visibility: hidden") + style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 @@ -155,7 +157,9 @@ block content span.small /mo ul.list-unstyled li - strong #{translate("collabs_per_proj", {collabcount:6})} + strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} + strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} + strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -176,7 +180,9 @@ block content span.small /yr ul.list-unstyled li - strong #{translate("collabs_per_proj", {collabcount:6})} + strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} + strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} + strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index f16cace816..c736014c8b 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -3,6 +3,15 @@ define [ "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> + App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)-> + $scope.$watch "ui.view", -> + if $scope.ui.view == "history" + if $scope.project?.features?.versioning + $scope.versioningPopupType = "default" + else if $scope.ui.view == "history" + sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> + $scope.versioningPopupType = chosenVariation + App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> $scope.hoveringOverListSelectors = false diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 638371e45e..0cabc224a0 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -19,8 +19,24 @@ define [ url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true - event_tracking.sendMB "subscription-start-trial", { source, plan } + switch source + when "dropbox" + sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) -> + event_tracking.sendMB "subscription-start-trial", { source, plan, variant } + + when "history" + sixpack.participate 'teaser-history', ['default', 'focused'], (variant) -> + event_tracking.sendMB "subscription-start-trial", { source, plan, variant } + + else + event_tracking.sendMB "subscription-start-trial", { source, plan } w.location = url - go() + if $scope.shouldABTestPlans + sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> + if chosenVariation in ['heron', 'ibis'] + plan = "collaborator_#{chosenVariation}" + go() + else + go() diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index b74a4d1da9..060517a9d5 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -11,6 +11,164 @@ define [ return { currencyCode:currencyCode + heron: + USD: + student: + monthly: "$6" + annual: "$60" + collaborator: + monthly: "$12" + annual: "$144" + EUR: + student: + monthly: "€5" + annual: "€50" + collaborator: + monthly: "€11" + annual: "€132" + GBP: + student: + monthly: "£5" + annual: "£50" + collaborator: + monthly: "£10" + annual: "£120" + SEK: + student: + monthly: "45 kr" + annual: "450 kr" + collaborator: + monthly: "90 kr" + annual: "1080 kr" + CAD: + student: + monthly: "$7" + annual: "$70" + collaborator: + monthly: "$14" + annual: "$168" + NOK: + student: + monthly: "45 kr" + annual: "450 kr" + collaborator: + monthly: "90 kr" + annual: "1080 kr" + DKK: + student: + monthly: "40 kr" + annual: "400 kr" + collaborator: + monthly: "70 kr" + annual: "840 kr" + AUD: + student: + monthly: "$8" + annual: "$80" + collaborator: + monthly: "$15" + annual: "$180" + NZD: + student: + monthly: "$8" + annual: "$80" + collaborator: + monthly: "$15" + annual: "$180" + CHF: + student: + monthly: "Fr 6" + annual: "Fr 60" + collaborator: + monthly: "Fr 12" + annual: "Fr 144" + SGD: + student: + monthly: "$8" + annual: "$80" + collaborator: + monthly: "$16" + annual: "$192" + + ibis: + USD: + student: + monthly: "$10" + annual: "$100" + collaborator: + monthly: "$18" + annual: "$216" + EUR: + student: + monthly: "€9" + annual: "€90" + collaborator: + monthly: "€17" + annual: "€204" + GBP: + student: + monthly: "£7" + annual: "£70" + collaborator: + monthly: "£14" + annual: "£168" + SEK: + student: + monthly: "75 kr" + annual: "750 kr" + collaborator: + monthly: "140 kr" + annual: "1680 kr" + CAD: + student: + monthly: "$12" + annual: "$120" + collaborator: + monthly: "$22" + annual: "$264" + NOK: + student: + monthly: "75 kr" + annual: "750 kr" + collaborator: + monthly: "140 kr" + annual: "1680 kr" + DKK: + student: + monthly: "68 kr" + annual: "680 kr" + collaborator: + monthly: "110 kr" + annual: "1320 kr" + AUD: + student: + monthly: "$13" + annual: "$130" + collaborator: + monthly: "$22" + annual: "$264" + NZD: + student: + monthly: "$14" + annual: "$140" + collaborator: + monthly: "$22" + annual: "$264" + CHF: + student: + monthly: "Fr 10" + annual: "Fr 100" + collaborator: + monthly: "Fr 18" + annual: "Fr 216" + SGD: + student: + monthly: "$14" + annual: "$140" + collaborator: + monthly: "$25" + annual: "$300" + plans: USD: symbol: "$" @@ -154,6 +312,14 @@ define [ $scope.shouldABTestPlans = window.shouldABTestPlans if $scope.shouldABTestPlans + sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> + $scope.plansVariant = chosenVariation + event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation} + if chosenVariation in ['heron', 'ibis'] + # overwrite student plans with alternative + for currency, _v of $scope.plans + $scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student'] + $scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator'] $scope.showPlans = true else $scope.showPlans = true diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee index 28d5af013b..c971b9712a 100644 --- a/services/web/public/coffee/main/subscription-dashboard.coffee +++ b/services/web/public/coffee/main/subscription-dashboard.coffee @@ -3,6 +3,8 @@ define [ ], (App)-> App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) -> + sixpack.convert 'plans-1610', () -> + SUBSCRIPTION_URL = "/user/subscription/update" From 18c6802b84ce099dcdb64e56059ea656d0173451 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 12 Apr 2018 13:53:26 -0500 Subject: [PATCH 32/47] Revert "Revert "Merge pull request #480 from sharelatex/jel-remove-old-exp"" This reverts commit 5c3f391041303456aeb800522c0fdcfe900625ac. --- .../web/app/views/project/editor/history.pug | 1 - services/web/app/views/project/editor/pdf.pug | 1 - .../web/app/views/project/editor/share.pug | 1 - .../web/app/views/project/list/side-bar.pug | 7 +- .../web/app/views/subscriptions/plans.pug | 18 +- .../controllers/HistoryListController.coffee | 9 - .../public/coffee/main/account-upgrade.coffee | 20 +-- services/web/public/coffee/main/plans.coffee | 166 ------------------ .../coffee/main/subscription-dashboard.coffee | 2 - 9 files changed, 10 insertions(+), 215 deletions(-) diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 7b4436af5d..48bef7adfb 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -33,7 +33,6 @@ div#history(ng-show="ui.view == 'history'") href ng-class="buttonClass" ng-click="startFreeTrial('history')" - sixpack-convert="teaser-history" ) #{translate("start_free_trial")} .message(ng-show="project.owner._id != user.id") diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug index a87b765cf2..a874a0b88b 100644 --- a/services/web/app/views/project/editor/pdf.pug +++ b/services/web/app/views/project/editor/pdf.pug @@ -402,7 +402,6 @@ div.full-size.pdf(ng-controller="PdfController") a.btn.btn-success.row-spaced-small( href ng-class="buttonClass" - sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index b63038cf86..d2aa1cc173 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -173,7 +173,6 @@ script(type='text/ng-template', id='shareProjectModalTemplate') a.btn.btn-success( href ng-class="buttonClass" - sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('projectMembers')" ) #{translate("start_free_trial")} diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index c4186a01d5..c695dfd30c 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -10,19 +10,16 @@ li a( href, - sixpack-convert="first_sign_up", ng-click="openCreateProjectModal()" ) #{translate("blank_project")} li a( href, - sixpack-convert="first_sign_up", ng-click="openCreateProjectModal('example')" ) #{translate("example_project")} li a( href, - sixpack-convert="first_sign_up", ng-click="openUploadProjectModal()" ) #{translate("upload_project")} != moduleIncludes("newProjectMenu", locals) @@ -31,7 +28,7 @@ li.dropdown-header #{translate("templates")} each item in templates li - a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} + a.menu-indent(href=item.url) #{translate(item.name)} .row-spaced(ng-if="projects.length > 0", ng-cloak) ul.list-unstyled.folders-menu( @@ -133,7 +130,7 @@ hr p.small #{translate("on_free_sl")} p - a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} + a(href="/user/subscription/plans").btn.btn-primary #{translate("upgrade")} p.small.text-centered | #{translate("or_unlock_features_bonus")} a(href="/user/bonus") #{translate("sharing_sl")} . diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index bafb855ff7..7c40c38a52 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -76,7 +76,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + style=(getLoggedInUserId() === null ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 .card.card-highlighted @@ -90,10 +90,8 @@ block content | {{plans[currencyCode]['collaborator']['annual']}} span.small /yr ul.list-unstyled - li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})} + li + strong #{translate("collabs_per_proj", {collabcount:10})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -144,7 +142,7 @@ block content br a.btn.btn-info( href="/register" - style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + style=(getLoggedInUserId() === null ? "" : "visibility: hidden") ) #{translate("sign_up_now")} .col-md-4 @@ -157,9 +155,7 @@ block content span.small /mo ul.list-unstyled li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} + strong #{translate("collabs_per_proj", {collabcount:6})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} @@ -180,9 +176,7 @@ block content span.small /yr ul.list-unstyled li - strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})} - strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})} - strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})} + strong #{translate("collabs_per_proj", {collabcount:6})} li #{translate("full_doc_history")} li #{translate("sync_to_dropbox")} li #{translate("sync_to_github")} diff --git a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index c736014c8b..f16cace816 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -3,15 +3,6 @@ define [ "ide/history/util/displayNameForUser" ], (App, displayNameForUser) -> - App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)-> - $scope.$watch "ui.view", -> - if $scope.ui.view == "history" - if $scope.project?.features?.versioning - $scope.versioningPopupType = "default" - else if $scope.ui.view == "history" - sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> - $scope.versioningPopupType = chosenVariation - App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> $scope.hoveringOverListSelectors = false diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 0cabc224a0..638371e45e 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -19,24 +19,8 @@ define [ url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true - switch source - when "dropbox" - sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) -> - event_tracking.sendMB "subscription-start-trial", { source, plan, variant } - - when "history" - sixpack.participate 'teaser-history', ['default', 'focused'], (variant) -> - event_tracking.sendMB "subscription-start-trial", { source, plan, variant } - - else - event_tracking.sendMB "subscription-start-trial", { source, plan } + event_tracking.sendMB "subscription-start-trial", { source, plan } w.location = url - if $scope.shouldABTestPlans - sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> - if chosenVariation in ['heron', 'ibis'] - plan = "collaborator_#{chosenVariation}" - go() - else - go() + go() diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index 060517a9d5..b74a4d1da9 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -11,164 +11,6 @@ define [ return { currencyCode:currencyCode - heron: - USD: - student: - monthly: "$6" - annual: "$60" - collaborator: - monthly: "$12" - annual: "$144" - EUR: - student: - monthly: "€5" - annual: "€50" - collaborator: - monthly: "€11" - annual: "€132" - GBP: - student: - monthly: "£5" - annual: "£50" - collaborator: - monthly: "£10" - annual: "£120" - SEK: - student: - monthly: "45 kr" - annual: "450 kr" - collaborator: - monthly: "90 kr" - annual: "1080 kr" - CAD: - student: - monthly: "$7" - annual: "$70" - collaborator: - monthly: "$14" - annual: "$168" - NOK: - student: - monthly: "45 kr" - annual: "450 kr" - collaborator: - monthly: "90 kr" - annual: "1080 kr" - DKK: - student: - monthly: "40 kr" - annual: "400 kr" - collaborator: - monthly: "70 kr" - annual: "840 kr" - AUD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$15" - annual: "$180" - NZD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$15" - annual: "$180" - CHF: - student: - monthly: "Fr 6" - annual: "Fr 60" - collaborator: - monthly: "Fr 12" - annual: "Fr 144" - SGD: - student: - monthly: "$8" - annual: "$80" - collaborator: - monthly: "$16" - annual: "$192" - - ibis: - USD: - student: - monthly: "$10" - annual: "$100" - collaborator: - monthly: "$18" - annual: "$216" - EUR: - student: - monthly: "€9" - annual: "€90" - collaborator: - monthly: "€17" - annual: "€204" - GBP: - student: - monthly: "£7" - annual: "£70" - collaborator: - monthly: "£14" - annual: "£168" - SEK: - student: - monthly: "75 kr" - annual: "750 kr" - collaborator: - monthly: "140 kr" - annual: "1680 kr" - CAD: - student: - monthly: "$12" - annual: "$120" - collaborator: - monthly: "$22" - annual: "$264" - NOK: - student: - monthly: "75 kr" - annual: "750 kr" - collaborator: - monthly: "140 kr" - annual: "1680 kr" - DKK: - student: - monthly: "68 kr" - annual: "680 kr" - collaborator: - monthly: "110 kr" - annual: "1320 kr" - AUD: - student: - monthly: "$13" - annual: "$130" - collaborator: - monthly: "$22" - annual: "$264" - NZD: - student: - monthly: "$14" - annual: "$140" - collaborator: - monthly: "$22" - annual: "$264" - CHF: - student: - monthly: "Fr 10" - annual: "Fr 100" - collaborator: - monthly: "Fr 18" - annual: "Fr 216" - SGD: - student: - monthly: "$14" - annual: "$140" - collaborator: - monthly: "$25" - annual: "$300" - plans: USD: symbol: "$" @@ -312,14 +154,6 @@ define [ $scope.shouldABTestPlans = window.shouldABTestPlans if $scope.shouldABTestPlans - sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)-> - $scope.plansVariant = chosenVariation - event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation} - if chosenVariation in ['heron', 'ibis'] - # overwrite student plans with alternative - for currency, _v of $scope.plans - $scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student'] - $scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator'] $scope.showPlans = true else $scope.showPlans = true diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee index c971b9712a..28d5af013b 100644 --- a/services/web/public/coffee/main/subscription-dashboard.coffee +++ b/services/web/public/coffee/main/subscription-dashboard.coffee @@ -3,8 +3,6 @@ define [ ], (App)-> App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) -> - sixpack.convert 'plans-1610', () -> - SUBSCRIPTION_URL = "/user/subscription/update" From 7e8b59cbb388ac646df3645d28bd979c4081cbb5 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 15:57:34 +0100 Subject: [PATCH 33/47] wip form loads as ifram --- services/web/app/views/subscriptions/new.pug | 208 +----------------- .../coffee/main/new-subscription.coffee | 4 +- 2 files changed, 13 insertions(+), 199 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 8995af6192..9a3206f205 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -1,7 +1,7 @@ extends ../layout block scripts - script(src="https://js.recurly.com/v3/recurly.js") + script(src="https://js.recurly.com/v4/recurly.js") script(type='text/javascript'). window.countryCode = '#{countryCode}' @@ -49,205 +49,17 @@ block content .row div() .col-md-12() - form( - ng-if="planName" - name="simpleCCForm" - novalidate - ) - div.payment-method-toggle - a.payment-method-toggle-switch( - href - ng-click="setPaymentMethod('credit_card');" - ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''" - ) - i.fa.fa-cc-mastercard.fa-2x - span   - i.fa.fa-cc-visa.fa-2x - span   - i.fa.fa-cc-amex.fa-2x - a.payment-method-toggle-switch( - href - ng-click="setPaymentMethod('paypal');" - ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''" - ) - i.fa.fa-cc-paypal.fa-2x - - .alert.alert-warning.small(ng-show="genericError") - strong {{genericError}} - - div(ng-if="paymentMethod.value === 'credit_card'") - .row - .col-xs-6 - .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") - label(for="first-name") #{translate('first_name')} - input#first-name.form-control( - type="text" - maxlength='255' - data-recurly="first_name" - name="firstName" - ng-model="data.first_name" - required - ) - span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }} - .col-xs-6 - .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''") - label(for="last-name") #{translate('last_name')} - input#last-name.form-control( - type="text" - maxlength='255' - data-recurly="last_name" - name="lastName" - ng-model="data.last_name" - required - ) - span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }} - - .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''") - label(for="card-no") #{translate("credit_card_number")} - input#card-no.form-control( - type="text" - ng-model="data.number" - name="ccNumber" - ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;" - ng-blur="validateCardNumber();" - required - cc-format-card-number - ) - span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }} - - .row - .col-xs-6 - .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") - label #{translate("expiry")} - input.form-control( - type="text" - ng-model="data.mmYY" - name="expiry" - placeholder="MM / YY" - ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" - ng-blur="updateExpiry(); validateExpiry()" - required - cc-format-expiry - ) - span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} - - .col-xs-6 - .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''") - label #{translate("security_code")} - input.form-control( - type="text" - ng-model="data.cvv" - ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" - ng-blur="validateCvv()" - name="cvv" - required - cc-format-sec-code - ) - .form-control-feedback - a.form-helper( - href - tabindex="-1" - tooltip-template="'cvv-tooltip-tpl.html'" - tooltip-trigger="mouseenter" - tooltip-append-to-body="true" - ) ? - span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }} - - - div - .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") - label(for="country") #{translate('country')} - select#country.form-control( - data-recurly="country" - ng-model="data.country" - name="country" - ng-change="updateCountry()" - required - ) - +countries_options() - span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }} - - if (showVatField) - .form-group - label(for="vat-no") #{translate('vat_number')} - input#vat-no.form-control( - type="text" - ng-blur="applyVatNumber()" - ng-model="data.vat_number" - ) - if (showCouponField) - .form-group - label(for="coupon-code") #{translate('coupon_code')} - input#coupon-code.form-control( - type="text" - ng-blur="applyCoupon()" - ng-model="data.coupon" - ) - - p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")} - - div.price-breakdown(ng-if="price.next.tax !== '0.00'") - hr.thin - span Total: - strong {{price.currency.symbol}}{{price.next.total}} - span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax) - span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")} - span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")} - hr.thin - - div.payment-submit - button.btn.btn-success.btn-block( - ng-click="submit()" - ng-disabled="processing || !isFormValid(simpleCCForm);" - ) - span(ng-show="processing") - i.fa.fa-spinner.fa-spin - |   - | {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }} + form(method='post', action='/api/subscriptions/new') + label(for='number') Card Number + #number(data-recurly='number') + label(for='month') Month + #month(data-recurly='month') + label(for='year') Year + #year(data-recurly='year') + button#subscribe Subscribe - .col-md-3.col-md-pull-4 - if showStudentPlan == 'true' - a.btn-primary.btn.plansPageStudentLink( - href, - ng-click="switchToStudent()" - ) #{translate("half_price_student")} - - .card.card-first - .paymentPageFeatures - h3 #{translate("unlimited_projects")} - p #{translate("create_unlimited_projects")} - - h3 - if plan.features.collaborators == -1 - - var collaboratorCount = 'Unlimited' - else - - var collaboratorCount = plan.features.collaborators - | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} - p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time. - - h3 #{translate("full_doc_history")} - p #{translate("see_what_has_been")} - span.added #{translate("added")} - | #{translate("and")} - span.removed #{translate("removed")}. - | #{translate("restore_to_any_older_version")}. - - h3 #{translate("sync_to_dropbox")} - p - | #{translate("acces_work_from_anywhere")}. - | #{translate("work_offline_and_sync_with_dropbox")}. - - hr - - p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days. - hr - span                   - a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;") - img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0") - div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;") - a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;") - + script(type="text/javascript"). ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}") diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index 6e8af93156..a2d5428c67 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -43,7 +43,7 @@ define [ $scope.processing = false - recurly.configure window.recurlyApiKey + recurly.configure publicKey:window.recurlyApiKey pricing = recurly.Pricing() window.pricing = pricing @@ -73,6 +73,8 @@ define [ $scope.normalPrice += (basePrice * pricing.price.taxes[0].rate) $scope.$apply() + + $scope.applyCoupon = -> pricing.coupon($scope.data.coupon).done() From ae3858bcd07bd658d464ca891e07edf7cb6f8776 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 16:29:01 +0100 Subject: [PATCH 34/47] can subscribe using new form. terrible styling --- services/web/app/views/subscriptions/new.pug | 219 +++++++++++++++++- .../coffee/main/new-subscription.coffee | 18 +- 2 files changed, 227 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 9a3206f205..987009fc0d 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -49,17 +49,218 @@ block content .row div() .col-md-12() - form(method='post', action='/api/subscriptions/new') - label(for='number') Card Number - #number(data-recurly='number') - label(for='month') Month - #month(data-recurly='month') - label(for='year') Year - #year(data-recurly='year') - button#subscribe Subscribe + form( + name="simpleCCForm" + novalidate + ) + + + div.payment-method-toggle + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('credit_card');" + ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-mastercard.fa-2x + span   + i.fa.fa-cc-visa.fa-2x + span   + i.fa.fa-cc-amex.fa-2x + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('paypal');" + ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-paypal.fa-2x + + .alert.alert-warning.small(ng-show="genericError") + strong {{genericError}} + + div() + .row + .col-xs-6 + .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") + label(for="first-name") #{translate('first_name')} + input#first-name.form-control( + type="text" + maxlength='255' + data-recurly="first_name" + name="firstName" + ng-model="data.first_name" + required + ) + span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }} + .col-xs-6 + .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''") + label(for="last-name") #{translate('last_name')} + input#last-name.form-control( + type="text" + maxlength='255' + data-recurly="last_name" + name="lastName" + ng-model="data.last_name" + required + ) + span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }} + + .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''") + label(for="card-no") #{translate("credit_card_number")} + div#card-no.form-control( + type="text" + name="ccNumber" + data-recurly='number' + ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;" + ng-blur="validateCardNumber();" + required + ) + span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }} + + .row + .col-xs-3 + .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") + label(for="month") #{translate("month")} + div.form-control( + type="text" + name="month" + ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" + ng-blur="updateExpiry(); validateExpiry()" + data-recurly="month" + required + ) + span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} + .col-xs-3 + .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") + label(for="year") #{translate("year")} + div.form-control( + type="text" + name="year" + data-recurly="year" + ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" + ng-blur="updateExpiry(); validateExpiry()" + required + ) + span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} + - + .col-xs-6 + .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''") + label #{translate("security_code")} + div.form-control( + type="text" + ng-model="data.cvv" + ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" + ng-blur="validateCvv()" + data-recurly="cvv" + name="cvv" + required + cc-format-sec-code + ) + .form-control-feedback + a.form-helper( + href + tabindex="-1" + tooltip-template="'cvv-tooltip-tpl.html'" + tooltip-trigger="mouseenter" + tooltip-append-to-body="true" + ) ? + span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }} + + + div + .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") + label(for="country") #{translate('country')} + select#country.form-control( + data-recurly="country" + ng-model="data.country" + name="country" + ng-change="updateCountry()" + required + ) + +countries_options() + span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }} + + if (showVatField) + .form-group + label(for="vat-no") #{translate('vat_number')} + input#vat-no.form-control( + type="text" + ng-blur="applyVatNumber()" + ng-model="data.vat_number" + ) + if (showCouponField) + .form-group + label(for="coupon-code") #{translate('coupon_code')} + input#coupon-code.form-control( + type="text" + ng-blur="applyCoupon()" + ng-model="data.coupon" + ) + + p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")} + + div.price-breakdown(ng-if="price.next.tax !== '0.00'") + hr.thin + span Total: + strong {{price.currency.symbol}}{{price.next.total}} + span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax) + span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")} + span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")} + hr.thin + + div.payment-submit + button.btn.btn-success.btn-block( + ng-click="submit()" + ng-disabled="processing" + ) + span(ng-show="processing") + i.fa.fa-spinner.fa-spin + |   + | {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }} + + + .col-md-3.col-md-pull-4 + if showStudentPlan == 'true' + a.btn-primary.btn.plansPageStudentLink( + href, + ng-click="switchToStudent()" + ) #{translate("half_price_student")} + + .card.card-first + .paymentPageFeatures + h3 #{translate("unlimited_projects")} + p #{translate("create_unlimited_projects")} + + h3 + if plan.features.collaborators == -1 + - var collaboratorCount = 'Unlimited' + else + - var collaboratorCount = plan.features.collaborators + | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} + p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time. + + h3 #{translate("full_doc_history")} + p #{translate("see_what_has_been")} + span.added #{translate("added")} + | #{translate("and")} + span.removed #{translate("removed")}. + | #{translate("restore_to_any_older_version")}. + + h3 #{translate("sync_to_dropbox")} + p + | #{translate("acces_work_from_anywhere")}. + | #{translate("work_offline_and_sync_with_dropbox")}. + + hr + + p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days. + hr + span                   + a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;") + img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0") + div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;") + a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;") + script(type="text/javascript"). ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}") diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index a2d5428c67..2925b280f4 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -43,7 +43,23 @@ define [ $scope.processing = false - recurly.configure publicKey:window.recurlyApiKey + recurly.configure + publicKey: window.recurlyApiKey + style: + all: + fontFamily: 'Open Sans', + fontSize: '1rem', + fontWeight: 'bold', + fontColor: '#2c0730' + number: + placeholder: 'Card number' + month: + placeholder: 'mm' + year: + placeholder: 'yy' + cvv: + placeholder: 'cvv' + pricing = recurly.Pricing() window.pricing = pricing From ccef0760aede3422453ff644edb2b9249cd16679 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 16:51:55 +0100 Subject: [PATCH 35/47] remove the form control, it just breaks the iframe styling --- services/web/app/views/subscriptions/new.pug | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 987009fc0d..5f679d0d8e 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -105,7 +105,7 @@ block content .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''") label(for="card-no") #{translate("credit_card_number")} - div#card-no.form-control( + div#card-no( type="text" name="ccNumber" data-recurly='number' @@ -119,7 +119,7 @@ block content .col-xs-3 .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") label(for="month") #{translate("month")} - div.form-control( + div( type="text" name="month" ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" @@ -131,7 +131,7 @@ block content .col-xs-3 .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") label(for="year") #{translate("year")} - div.form-control( + div( type="text" name="year" data-recurly="year" @@ -146,7 +146,7 @@ block content .col-xs-6 .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''") label #{translate("security_code")} - div.form-control( + div( type="text" ng-model="data.cvv" ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" From c050791905aa52f7b1ec66c35fad75d8ec1777ee Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 16:57:33 +0100 Subject: [PATCH 36/47] change ng to use show/hide ng-if doesn't render html until evaulated to true, need the form to be there at time of recurly.configure --- services/web/app/views/subscriptions/new.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 5f679d0d8e..27908d4c2c 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -76,7 +76,7 @@ block content .alert.alert-warning.small(ng-show="genericError") strong {{genericError}} - div() + div(ng-show="paymentMethod.value === 'credit_card'") .row .col-xs-6 .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") From e19a118ef4d4355d0585880463df564986f36a12 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 17:26:05 +0100 Subject: [PATCH 37/47] poorly styled but almost viable --- services/web/public/stylesheets/app/recurly.less | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/web/public/stylesheets/app/recurly.less b/services/web/public/stylesheets/app/recurly.less index c953f34bd1..49221f913d 100644 --- a/services/web/public/stylesheets/app/recurly.less +++ b/services/web/public/stylesheets/app/recurly.less @@ -1,3 +1,10 @@ +.recurly-hosted-field { + height: 2em; + border: 1px solid #ccc; + +} + + .recurly { display: block; position: relative; From b1ee05de3ea34f3c74f218d2955dadd793f0897e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 13 Apr 2018 17:32:20 +0100 Subject: [PATCH 38/47] better styling, close to being deployable --- services/web/public/coffee/main/new-subscription.coffee | 8 ++------ services/web/public/stylesheets/app/recurly.less | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index 2925b280f4..e5e0165163 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -47,12 +47,8 @@ define [ publicKey: window.recurlyApiKey style: all: - fontFamily: 'Open Sans', - fontSize: '1rem', - fontWeight: 'bold', - fontColor: '#2c0730' - number: - placeholder: 'Card number' + fontFamily: '"Open Sans", sans-serif', + fontSize: '16px', month: placeholder: 'mm' year: diff --git a/services/web/public/stylesheets/app/recurly.less b/services/web/public/stylesheets/app/recurly.less index 49221f913d..2e88d6742d 100644 --- a/services/web/public/stylesheets/app/recurly.less +++ b/services/web/public/stylesheets/app/recurly.less @@ -1,7 +1,5 @@ .recurly-hosted-field { - height: 2em; - border: 1px solid #ccc; - + &:extend(.form-control); } From a1be0e95b421934f4a6d91bb1f327b18cf20ba33 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:10:25 +0100 Subject: [PATCH 39/47] remove the dead validation --- services/web/app/views/subscriptions/new.pug | 22 +++------ .../coffee/main/new-subscription.coffee | 46 ++++--------------- 2 files changed, 15 insertions(+), 53 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 27908d4c2c..5644710723 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -101,9 +101,8 @@ block content ng-model="data.last_name" required ) - span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }} - .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''") + .form-group() label(for="card-no") #{translate("credit_card_number")} div#card-no( type="text" @@ -113,41 +112,36 @@ block content ng-blur="validateCardNumber();" required ) - span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }} .row .col-xs-3 - .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") + .form-group.has-feedback() label(for="month") #{translate("month")} div( - type="text" + type="number" name="month" ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" ng-blur="updateExpiry(); validateExpiry()" data-recurly="month" required ) - span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} .col-xs-3 - .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") + .form-group.has-feedback() label(for="year") #{translate("year")} div( - type="text" + type="number" name="year" data-recurly="year" ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" ng-blur="updateExpiry(); validateExpiry()" required ) - span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} - - .col-xs-6 - .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''") + .form-group.has-feedback() label #{translate("security_code")} div( - type="text" + type="number" ng-model="data.cvv" ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" ng-blur="validateCvv()" @@ -164,9 +158,7 @@ block content tooltip-trigger="mouseenter" tooltip-append-to-body="true" ) ? - span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }} - div .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") label(for="country") #{translate('country')} diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index e5e0165163..ab1128ed33 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -21,10 +21,6 @@ define [ value: "credit_card" $scope.data = - number: "" - month: "" - year: "" - cvv: "" first_name: "" last_name: "" postal_code: "" @@ -34,12 +30,8 @@ define [ city:"" country:window.countryCode coupon: window.couponCode - mmYY: "" - - $scope.validation = - correctCardNumber : true - correctExpiry: true - correctCvv: true + + $scope.validation = {} $scope.processing = false @@ -49,13 +41,14 @@ define [ all: fontFamily: '"Open Sans", sans-serif', fontSize: '16px', + fontColor: '#7a7a7a' month: - placeholder: 'mm' + placeholder: 'MM' year: - placeholder: 'yy' + placeholder: 'YY' cvv: - placeholder: 'cvv' - + placeholder: 'CVV' + pricing = recurly.Pricing() window.pricing = pricing @@ -97,26 +90,6 @@ define [ $scope.currencyCode = newCurrency pricing.currency(newCurrency).done() - $scope.updateExpiry = () -> - parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY - if parsedDateObj? - $scope.data.month = parsedDateObj.month - $scope.data.year = parsedDateObj.year - - $scope.validateCardNumber = validateCardNumber = -> - $scope.validation.errorFields = {} - if $scope.data.number?.length != 0 - $scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number) - - $scope.validateExpiry = validateExpiry = -> - $scope.validation.errorFields = {} - if $scope.data.month?.length != 0 and $scope.data.year?.length != 0 - $scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year) - - $scope.validateCvv = validateCvv = -> - $scope.validation.errorFields = {} - if $scope.data.cvv?.length != 0 - $scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv) $scope.inputHasError = inputHasError = (formItem) -> if !formItem? @@ -128,10 +101,7 @@ define [ if $scope.paymentMethod.value == 'paypal' return $scope.data.country != "" else - return (form.$valid and - $scope.validation.correctCardNumber and - $scope.validation.correctExpiry and - $scope.validation.correctCvv) + return form.$valid $scope.updateCountry = -> pricing.address({country:$scope.data.country}).done() From 903a9db936fae37c75d7ff2f4d016cc7c4761010 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:10:43 +0100 Subject: [PATCH 40/47] upgrade static recurly lib to 4.8.5 for plans page --- services/web/public/coffee/main/plans.coffee | 2 +- services/web/public/js/libs/recurly-3.0.5.js | 4218 ------------------ services/web/public/js/libs/recurly-4.8.5.js | 6 + services/web/public/js/libs/recurly.min.js | 1 - 4 files changed, 7 insertions(+), 4220 deletions(-) delete mode 100644 services/web/public/js/libs/recurly-3.0.5.js create mode 100644 services/web/public/js/libs/recurly-4.8.5.js delete mode 100755 services/web/public/js/libs/recurly.min.js diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee index b74a4d1da9..9a62420d66 100644 --- a/services/web/public/coffee/main/plans.coffee +++ b/services/web/public/coffee/main/plans.coffee @@ -1,6 +1,6 @@ define [ "base" - "libs/recurly-3.0.5" + "libs/recurly-4.8.5" ], (App, recurly) -> diff --git a/services/web/public/js/libs/recurly-3.0.5.js b/services/web/public/js/libs/recurly-3.0.5.js deleted file mode 100644 index 9ae06622aa..0000000000 --- a/services/web/public/js/libs/recurly-3.0.5.js +++ /dev/null @@ -1,4218 +0,0 @@ -;(function(){ - -/** - * Require the given path. - * - * @param {String} path - * @return {Object} exports - * @api public - */ - -function require(path, parent, orig) { - var resolved = require.resolve(path); - - // lookup failed - if (null == resolved) { - orig = orig || path; - parent = parent || 'root'; - var err = new Error('Failed to require "' + orig + '" from "' + parent + '"'); - err.path = orig; - err.parent = parent; - err.require = true; - throw err; - } - - var module = require.modules[resolved]; - - // perform real require() - // by invoking the module's - // registered function - if (!module._resolving && !module.exports) { - var mod = {}; - mod.exports = {}; - mod.client = mod.component = true; - module._resolving = true; - module.call(this, mod.exports, require.relative(resolved), mod); - delete module._resolving; - module.exports = mod.exports; - } - - return module.exports; -} - -/** - * Registered modules. - */ - -require.modules = {}; - -/** - * Registered aliases. - */ - -require.aliases = {}; - -/** - * Resolve `path`. - * - * Lookup: - * - * - PATH/index.js - * - PATH.js - * - PATH - * - * @param {String} path - * @return {String} path or null - * @api private - */ - -require.resolve = function(path) { - if (path.charAt(0) === '/') path = path.slice(1); - - var paths = [ - path, - path + '.js', - path + '.json', - path + '/index.js', - path + '/index.json' - ]; - - for (var i = 0; i < paths.length; i++) { - var path = paths[i]; - if (require.modules.hasOwnProperty(path)) return path; - if (require.aliases.hasOwnProperty(path)) return require.aliases[path]; - } -}; - -/** - * Normalize `path` relative to the current path. - * - * @param {String} curr - * @param {String} path - * @return {String} - * @api private - */ - -require.normalize = function(curr, path) { - var segs = []; - - if ('.' != path.charAt(0)) return path; - - curr = curr.split('/'); - path = path.split('/'); - - for (var i = 0; i < path.length; ++i) { - if ('..' == path[i]) { - curr.pop(); - } else if ('.' != path[i] && '' != path[i]) { - segs.push(path[i]); - } - } - - return curr.concat(segs).join('/'); -}; - -/** - * Register module at `path` with callback `definition`. - * - * @param {String} path - * @param {Function} definition - * @api private - */ - -require.register = function(path, definition) { - require.modules[path] = definition; -}; - -/** - * Alias a module definition. - * - * @param {String} from - * @param {String} to - * @api private - */ - -require.alias = function(from, to) { - if (!require.modules.hasOwnProperty(from)) { - throw new Error('Failed to alias "' + from + '", it does not exist'); - } - require.aliases[to] = from; -}; - -/** - * Return a require function relative to the `parent` path. - * - * @param {String} parent - * @return {Function} - * @api private - */ - -require.relative = function(parent) { - var p = require.normalize(parent, '..'); - - /** - * lastIndexOf helper. - */ - - function lastIndexOf(arr, obj) { - var i = arr.length; - while (i--) { - if (arr[i] === obj) return i; - } - return -1; - } - - /** - * The relative require() itself. - */ - - function localRequire(path) { - var resolved = localRequire.resolve(path); - return require(resolved, parent, path); - } - - /** - * Resolve relative to the parent. - */ - - localRequire.resolve = function(path) { - var c = path.charAt(0); - if ('/' == c) return path.slice(1); - if ('.' == c) return require.normalize(p, path); - - // resolve deps by returning - // the dep in the nearest "deps" - // directory - var segs = parent.split('/'); - var i = lastIndexOf(segs, 'deps') + 1; - if (!i) i = 0; - path = segs.slice(0, i + 1).join('/') + '/deps/' + path; - return path; - }; - - /** - * Check if module is defined at `path`. - */ - - localRequire.exists = function(path) { - return require.modules.hasOwnProperty(localRequire.resolve(path)); - }; - - return localRequire; -}; -require.register("visionmedia-node-querystring/index.js", function(exports, require, module){ -/** - * Object#toString() ref for stringify(). - */ - -var toString = Object.prototype.toString; - -/** - * Object#hasOwnProperty ref - */ - -var hasOwnProperty = Object.prototype.hasOwnProperty; - -/** - * Array#indexOf shim. - */ - -var indexOf = typeof Array.prototype.indexOf === 'function' - ? function(arr, el) { return arr.indexOf(el); } - : function(arr, el) { - for (var i = 0; i < arr.length; i++) { - if (arr[i] === el) return i; - } - return -1; - }; - -/** - * Array.isArray shim. - */ - -var isArray = Array.isArray || function(arr) { - return toString.call(arr) == '[object Array]'; -}; - -/** - * Object.keys shim. - */ - -var objectKeys = Object.keys || function(obj) { - var ret = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - ret.push(key); - } - } - return ret; -}; - -/** - * Array#forEach shim. - */ - -var forEach = typeof Array.prototype.forEach === 'function' - ? function(arr, fn) { return arr.forEach(fn); } - : function(arr, fn) { - for (var i = 0; i < arr.length; i++) fn(arr[i]); - }; - -/** - * Array#reduce shim. - */ - -var reduce = function(arr, fn, initial) { - if (typeof arr.reduce === 'function') return arr.reduce(fn, initial); - var res = initial; - for (var i = 0; i < arr.length; i++) res = fn(res, arr[i]); - return res; -}; - -/** - * Cache non-integer test regexp. - */ - -var isint = /^[0-9]+$/; - -function promote(parent, key) { - if (parent[key].length == 0) return parent[key] = {} - var t = {}; - for (var i in parent[key]) { - if (hasOwnProperty.call(parent[key], i)) { - t[i] = parent[key][i]; - } - } - parent[key] = t; - return t; -} - -function parse(parts, parent, key, val) { - var part = parts.shift(); - - // illegal - if (Object.getOwnPropertyDescriptor(Object.prototype, key)) return; - - // end - if (!part) { - if (isArray(parent[key])) { - parent[key].push(val); - } else if ('object' == typeof parent[key]) { - parent[key] = val; - } else if ('undefined' == typeof parent[key]) { - parent[key] = val; - } else { - parent[key] = [parent[key], val]; - } - // array - } else { - var obj = parent[key] = parent[key] || []; - if (']' == part) { - if (isArray(obj)) { - if ('' != val) obj.push(val); - } else if ('object' == typeof obj) { - obj[objectKeys(obj).length] = val; - } else { - obj = parent[key] = [parent[key], val]; - } - // prop - } else if (~indexOf(part, ']')) { - part = part.substr(0, part.length - 1); - if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); - parse(parts, obj, part, val); - // key - } else { - if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); - parse(parts, obj, part, val); - } - } -} - -/** - * Merge parent key/val pair. - */ - -function merge(parent, key, val){ - if (~indexOf(key, ']')) { - var parts = key.split('[') - , len = parts.length - , last = len - 1; - parse(parts, parent, 'base', val); - // optimize - } else { - if (!isint.test(key) && isArray(parent.base)) { - var t = {}; - for (var k in parent.base) t[k] = parent.base[k]; - parent.base = t; - } - set(parent.base, key, val); - } - - return parent; -} - -/** - * Compact sparse arrays. - */ - -function compact(obj) { - if ('object' != typeof obj) return obj; - - if (isArray(obj)) { - var ret = []; - - for (var i in obj) { - if (hasOwnProperty.call(obj, i)) { - ret.push(obj[i]); - } - } - - return ret; - } - - for (var key in obj) { - obj[key] = compact(obj[key]); - } - - return obj; -} - -/** - * Parse the given obj. - */ - -function parseObject(obj){ - var ret = { base: {} }; - - forEach(objectKeys(obj), function(name){ - merge(ret, name, obj[name]); - }); - - return compact(ret.base); -} - -/** - * Parse the given str. - */ - -function parseString(str){ - var ret = reduce(String(str).split('&'), function(ret, pair){ - var eql = indexOf(pair, '=') - , brace = lastBraceInKey(pair) - , key = pair.substr(0, brace || eql) - , val = pair.substr(brace || eql, pair.length) - , val = val.substr(indexOf(val, '=') + 1, val.length); - - // ?foo - if ('' == key) key = pair, val = ''; - if ('' == key) return ret; - - return merge(ret, decode(key), decode(val)); - }, { base: {} }).base; - - return compact(ret); -} - -/** - * Parse the given query `str` or `obj`, returning an object. - * - * @param {String} str | {Object} obj - * @return {Object} - * @api public - */ - -exports.parse = function(str){ - if (null == str || '' == str) return {}; - return 'object' == typeof str - ? parseObject(str) - : parseString(str); -}; - -/** - * Turn the given `obj` into a query string - * - * @param {Object} obj - * @return {String} - * @api public - */ - -var stringify = exports.stringify = function(obj, prefix) { - if (isArray(obj)) { - return stringifyArray(obj, prefix); - } else if ('[object Object]' == toString.call(obj)) { - return stringifyObject(obj, prefix); - } else if ('string' == typeof obj) { - return stringifyString(obj, prefix); - } else { - return prefix + '=' + encodeURIComponent(String(obj)); - } -}; - -/** - * Stringify the given `str`. - * - * @param {String} str - * @param {String} prefix - * @return {String} - * @api private - */ - -function stringifyString(str, prefix) { - if (!prefix) throw new TypeError('stringify expects an object'); - return prefix + '=' + encodeURIComponent(str); -} - -/** - * Stringify the given `arr`. - * - * @param {Array} arr - * @param {String} prefix - * @return {String} - * @api private - */ - -function stringifyArray(arr, prefix) { - var ret = []; - if (!prefix) throw new TypeError('stringify expects an object'); - for (var i = 0; i < arr.length; i++) { - ret.push(stringify(arr[i], prefix + '[' + i + ']')); - } - return ret.join('&'); -} - -/** - * Stringify the given `obj`. - * - * @param {Object} obj - * @param {String} prefix - * @return {String} - * @api private - */ - -function stringifyObject(obj, prefix) { - var ret = [] - , keys = objectKeys(obj) - , key; - - for (var i = 0, len = keys.length; i < len; ++i) { - key = keys[i]; - if ('' == key) continue; - if (null == obj[key]) { - ret.push(encodeURIComponent(key) + '='); - } else { - ret.push(stringify(obj[key], prefix - ? prefix + '[' + encodeURIComponent(key) + ']' - : encodeURIComponent(key))); - } - } - - return ret.join('&'); -} - -/** - * Set `obj`'s `key` to `val` respecting - * the weird and wonderful syntax of a qs, - * where "foo=bar&foo=baz" becomes an array. - * - * @param {Object} obj - * @param {String} key - * @param {String} val - * @api private - */ - -function set(obj, key, val) { - var v = obj[key]; - if (Object.getOwnPropertyDescriptor(Object.prototype, key)) return; - if (undefined === v) { - obj[key] = val; - } else if (isArray(v)) { - v.push(val); - } else { - obj[key] = [v, val]; - } -} - -/** - * Locate last brace in `str` within the key. - * - * @param {String} str - * @return {Number} - * @api private - */ - -function lastBraceInKey(str) { - var len = str.length - , brace - , c; - for (var i = 0; i < len; ++i) { - c = str[i]; - if (']' == c) brace = false; - if ('[' == c) brace = true; - if ('=' == c && !brace) return i; - } -} - -/** - * Decode `str`. - * - * @param {String} str - * @return {String} - * @api private - */ - -function decode(str) { - try { - return decodeURIComponent(str.replace(/\+/g, ' ')); - } catch (err) { - return str; - } -} - -}); -require.register("component-emitter/index.js", function(exports, require, module){ - -/** - * Expose `Emitter`. - */ - -module.exports = Emitter; - -/** - * Initialize a new `Emitter`. - * - * @api public - */ - -function Emitter(obj) { - if (obj) return mixin(obj); -}; - -/** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - -function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; -} - -/** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.on = -Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; -}; - -/** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - - function on() { - self.off(event, on); - fn.apply(this, arguments); - } - - on.fn = fn; - this.on(event, on); - return this; -}; - -/** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.off = -Emitter.prototype.removeListener = -Emitter.prototype.removeAllListeners = -Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } - - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; -}; - -/** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ - -Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); - } - } - - return this; -}; - -/** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ - -Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; -}; - -/** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ - -Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; -}; - -}); -require.register("component-indexof/index.js", function(exports, require, module){ -module.exports = function(arr, obj){ - if (arr.indexOf) return arr.indexOf(obj); - for (var i = 0; i < arr.length; ++i) { - if (arr[i] === obj) return i; - } - return -1; -}; -}); -require.register("component-object/index.js", function(exports, require, module){ - -/** - * HOP ref. - */ - -var has = Object.prototype.hasOwnProperty; - -/** - * Return own keys in `obj`. - * - * @param {Object} obj - * @return {Array} - * @api public - */ - -exports.keys = Object.keys || function(obj){ - var keys = []; - for (var key in obj) { - if (has.call(obj, key)) { - keys.push(key); - } - } - return keys; -}; - -/** - * Return own values in `obj`. - * - * @param {Object} obj - * @return {Array} - * @api public - */ - -exports.values = function(obj){ - var vals = []; - for (var key in obj) { - if (has.call(obj, key)) { - vals.push(obj[key]); - } - } - return vals; -}; - -/** - * Merge `b` into `a`. - * - * @param {Object} a - * @param {Object} b - * @return {Object} a - * @api public - */ - -exports.merge = function(a, b){ - for (var key in b) { - if (has.call(b, key)) { - a[key] = b[key]; - } - } - return a; -}; - -/** - * Return length of `obj`. - * - * @param {Object} obj - * @return {Number} - * @api public - */ - -exports.length = function(obj){ - return exports.keys(obj).length; -}; - -/** - * Check if `obj` is empty. - * - * @param {Object} obj - * @return {Boolean} - * @api public - */ - -exports.isEmpty = function(obj){ - return 0 == exports.length(obj); -}; -}); -require.register("component-event/index.js", function(exports, require, module){ -var bind = window.addEventListener ? 'addEventListener' : 'attachEvent', - unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent', - prefix = bind !== 'addEventListener' ? 'on' : ''; - -/** - * Bind `el` event `type` to `fn`. - * - * @param {Element} el - * @param {String} type - * @param {Function} fn - * @param {Boolean} capture - * @return {Function} - * @api public - */ - -exports.bind = function(el, type, fn, capture){ - el[bind](prefix + type, fn, capture || false); - return fn; -}; - -/** - * Unbind `el` event `type`'s callback `fn`. - * - * @param {Element} el - * @param {String} type - * @param {Function} fn - * @param {Boolean} capture - * @return {Function} - * @api public - */ - -exports.unbind = function(el, type, fn, capture){ - el[unbind](prefix + type, fn, capture || false); - return fn; -}; -}); -require.register("component-clone/index.js", function(exports, require, module){ -/** - * Module dependencies. - */ - -var type; -try { - type = require('component-type'); -} catch (_) { - type = require('type'); -} - -/** - * Module exports. - */ - -module.exports = clone; - -/** - * Clones objects. - * - * @param {Mixed} any object - * @api public - */ - -function clone(obj){ - switch (type(obj)) { - case 'object': - var copy = {}; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - copy[key] = clone(obj[key]); - } - } - return copy; - - case 'array': - var copy = new Array(obj.length); - for (var i = 0, l = obj.length; i < l; i++) { - copy[i] = clone(obj[i]); - } - return copy; - - case 'regexp': - // from millermedeiros/amd-utils - MIT - var flags = ''; - flags += obj.multiline ? 'm' : ''; - flags += obj.global ? 'g' : ''; - flags += obj.ignoreCase ? 'i' : ''; - return new RegExp(obj.source, flags); - - case 'date': - return new Date(obj.getTime()); - - default: // string, number, boolean, … - return obj; - } -} - -}); -require.register("component-bind/index.js", function(exports, require, module){ - -/** - * Slice reference. - */ - -var slice = [].slice; - -/** - * Bind `obj` to `fn`. - * - * @param {Object} obj - * @param {Function|String} fn or string - * @return {Function} - * @api public - */ - -module.exports = function(obj, fn){ - if ('string' == typeof fn) fn = obj[fn]; - if ('function' != typeof fn) throw new Error('bind() requires a function'); - var args = [].slice.call(arguments, 2); - return function(){ - return fn.apply(obj, args.concat(slice.call(arguments))); - } -}; - -}); -require.register("component-props/index.js", function(exports, require, module){ -/** - * Global Names - */ - -var globals = /\b(this|Array|Date|Object|Math|JSON)\b/g; - -/** - * Return immediate identifiers parsed from `str`. - * - * @param {String} str - * @param {String|Function} map function or prefix - * @return {Array} - * @api public - */ - -module.exports = function(str, fn){ - var p = unique(props(str)); - if (fn && 'string' == typeof fn) fn = prefixed(fn); - if (fn) return map(str, p, fn); - return p; -}; - -/** - * Return immediate identifiers in `str`. - * - * @param {String} str - * @return {Array} - * @api private - */ - -function props(str) { - return str - .replace(/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\//g, '') - .replace(globals, '') - .match(/[$a-zA-Z_]\w*/g) - || []; -} - -/** - * Return `str` with `props` mapped with `fn`. - * - * @param {String} str - * @param {Array} props - * @param {Function} fn - * @return {String} - * @api private - */ - -function map(str, props, fn) { - var re = /\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\/|[a-zA-Z_]\w*/g; - return str.replace(re, function(_){ - if ('(' == _[_.length - 1]) return fn(_); - if (!~props.indexOf(_)) return _; - return fn(_); - }); -} - -/** - * Return unique array. - * - * @param {Array} arr - * @return {Array} - * @api private - */ - -function unique(arr) { - var ret = []; - - for (var i = 0; i < arr.length; i++) { - if (~ret.indexOf(arr[i])) continue; - ret.push(arr[i]); - } - - return ret; -} - -/** - * Map with prefix `str`. - */ - -function prefixed(str) { - return function(_){ - return str + _; - }; -} - -}); -require.register("component-to-function/index.js", function(exports, require, module){ -/** - * Module Dependencies - */ - -var expr = require('props'); - -/** - * Expose `toFunction()`. - */ - -module.exports = toFunction; - -/** - * Convert `obj` to a `Function`. - * - * @param {Mixed} obj - * @return {Function} - * @api private - */ - -function toFunction(obj) { - switch ({}.toString.call(obj)) { - case '[object Object]': - return objectToFunction(obj); - case '[object Function]': - return obj; - case '[object String]': - return stringToFunction(obj); - case '[object RegExp]': - return regexpToFunction(obj); - default: - return defaultToFunction(obj); - } -} - -/** - * Default to strict equality. - * - * @param {Mixed} val - * @return {Function} - * @api private - */ - -function defaultToFunction(val) { - return function(obj){ - return val === obj; - } -} - -/** - * Convert `re` to a function. - * - * @param {RegExp} re - * @return {Function} - * @api private - */ - -function regexpToFunction(re) { - return function(obj){ - return re.test(obj); - } -} - -/** - * Convert property `str` to a function. - * - * @param {String} str - * @return {Function} - * @api private - */ - -function stringToFunction(str) { - // immediate such as "> 20" - if (/^ *\W+/.test(str)) return new Function('_', 'return _ ' + str); - - // properties such as "name.first" or "age > 18" or "age > 18 && age < 36" - return new Function('_', 'return ' + get(str)); -} - -/** - * Convert `object` to a function. - * - * @param {Object} object - * @return {Function} - * @api private - */ - -function objectToFunction(obj) { - var match = {} - for (var key in obj) { - match[key] = typeof obj[key] === 'string' - ? defaultToFunction(obj[key]) - : toFunction(obj[key]) - } - return function(val){ - if (typeof val !== 'object') return false; - for (var key in match) { - if (!(key in val)) return false; - if (!match[key](val[key])) return false; - } - return true; - } -} - -/** - * Built the getter function. Supports getter style functions - * - * @param {String} str - * @return {String} - * @api private - */ - -function get(str) { - var props = expr(str); - if (!props.length) return '_.' + str; - - var val; - for(var i = 0, prop; prop = props[i]; i++) { - val = '_.' + prop; - val = "('function' == typeof " + val + " ? " + val + "() : " + val + ")"; - str = str.replace(new RegExp(prop, 'g'), val); - } - - return str; -} - -}); -require.register("component-each/index.js", function(exports, require, module){ - -/** - * Module dependencies. - */ - -var type = require('type'); -var toFunction = require('to-function'); - -/** - * HOP reference. - */ - -var has = Object.prototype.hasOwnProperty; - -/** - * Iterate the given `obj` and invoke `fn(val, i)` - * in optional context `ctx`. - * - * @param {String|Array|Object} obj - * @param {Function} fn - * @param {Object} [ctx] - * @api public - */ - -module.exports = function(obj, fn, ctx){ - fn = toFunction(fn); - ctx = ctx || this; - switch (type(obj)) { - case 'array': - return array(obj, fn, ctx); - case 'object': - if ('number' == typeof obj.length) return array(obj, fn, ctx); - return object(obj, fn, ctx); - case 'string': - return string(obj, fn, ctx); - } -}; - -/** - * Iterate string chars. - * - * @param {String} obj - * @param {Function} fn - * @param {Object} ctx - * @api private - */ - -function string(obj, fn, ctx) { - for (var i = 0; i < obj.length; ++i) { - fn.call(ctx, obj.charAt(i), i); - } -} - -/** - * Iterate object keys. - * - * @param {Object} obj - * @param {Function} fn - * @param {Object} ctx - * @api private - */ - -function object(obj, fn, ctx) { - for (var key in obj) { - if (has.call(obj, key)) { - fn.call(ctx, key, obj[key]); - } - } -} - -/** - * Iterate array-ish. - * - * @param {Array|Object} obj - * @param {Function} fn - * @param {Object} ctx - * @api private - */ - -function array(obj, fn, ctx) { - for (var i = 0; i < obj.length; ++i) { - fn.call(ctx, obj[i], i); - } -} - -}); -require.register("component-find/index.js", function(exports, require, module){ - -/** - * Module dependencies. - */ - -var toFunction = require('to-function'); - -/** - * Find the first value in `arr` with when `fn(val, i)` is truthy. - * - * @param {Array} arr - * @param {Function} fn - * @return {Array} - * @api public - */ - -module.exports = function(arr, fn){ - // callback - if ('function' != typeof fn) { - if (Object(fn) === fn) fn = objectToFunction(fn); - else fn = toFunction(fn); - } - - // filter - for (var i = 0, len = arr.length; i < len; ++i) { - if (fn(arr[i], i)) return arr[i]; - } -}; - -/** - * Convert `obj` into a match function. - * - * @param {Object} obj - * @return {Function} - * @api private - */ - -function objectToFunction(obj) { - return function(o){ - for (var key in obj) { - if (o[key] != obj[key]) return false; - } - return true; - } -} -}); -require.register("component-json/index.js", function(exports, require, module){ - -module.exports = 'undefined' == typeof JSON - ? require('component-json-fallback') - : JSON; - -}); -require.register("component-type/index.js", function(exports, require, module){ - -/** - * toString ref. - */ - -var toString = Object.prototype.toString; - -/** - * Return the type of `val`. - * - * @param {Mixed} val - * @return {String} - * @api public - */ - -module.exports = function(val){ - switch (toString.call(val)) { - case '[object Function]': return 'function'; - case '[object Date]': return 'date'; - case '[object RegExp]': return 'regexp'; - case '[object Arguments]': return 'arguments'; - case '[object Array]': return 'array'; - case '[object String]': return 'string'; - } - - if (val === null) return 'null'; - if (val === undefined) return 'undefined'; - if (val && val.nodeType === 1) return 'element'; - if (val === Object(val)) return 'object'; - - return typeof val; -}; - -}); -require.register("component-trim/index.js", function(exports, require, module){ - -exports = module.exports = trim; - -function trim(str){ - if (str.trim) return str.trim(); - return str.replace(/^\s*|\s*$/g, ''); -} - -exports.left = function(str){ - if (str.trimLeft) return str.trimLeft(); - return str.replace(/^\s*/, ''); -}; - -exports.right = function(str){ - if (str.trimRight) return str.trimRight(); - return str.replace(/\s*$/, ''); -}; - -}); -require.register("component-map/index.js", function(exports, require, module){ - -/** - * Module dependencies. - */ - -var toFunction = require('to-function'); - -/** - * Map the given `arr` with callback `fn(val, i)`. - * - * @param {Array} arr - * @param {Function} fn - * @return {Array} - * @api public - */ - -module.exports = function(arr, fn){ - var ret = []; - fn = toFunction(fn); - for (var i = 0; i < arr.length; ++i) { - ret.push(fn(arr[i], i)); - } - return ret; -}; -}); -require.register("yields-merge/index.js", function(exports, require, module){ - -/** - * merge `b`'s properties with `a`'s. - * - * example: - * - * var user = {}; - * merge(user, console); - * // > { log: fn, dir: fn ..} - * - * @param {Object} a - * @param {Object} b - * @return {Object} - */ - -module.exports = function (a, b) { - for (var k in b) a[k] = b[k]; - return a; -}; - -}); -require.register("learnboost-jsonp/index.js", function(exports, require, module){ -/** - * Module dependencies - */ - -var debug = require('debug')('jsonp'); - -/** - * Module exports. - */ - -module.exports = jsonp; - -/** - * Callback index. - */ - -var count = 0; - -/** - * Noop function. - */ - -function noop(){} - -/** - * JSONP handler - * - * Options: - * - param {String} qs parameter (`callback`) - * - timeout {Number} how long after a timeout error is emitted (`60000`) - * - * @param {String} url - * @param {Object|Function} optional options / callback - * @param {Function} optional callback - */ - -function jsonp(url, opts, fn){ - if ('function' == typeof opts) { - fn = opts; - opts = {}; - } - if (!opts) opts = {}; - - var prefix = opts.prefix || '__jp'; - var param = opts.param || 'callback'; - var timeout = null != opts.timeout ? opts.timeout : 60000; - var enc = encodeURIComponent; - var target = document.getElementsByTagName('script')[0] || document.head; - var script; - var timer; - - // generate a unique id for this request - var id = prefix + (count++); - - if (timeout) { - timer = setTimeout(function(){ - cleanup(); - if (fn) fn(new Error('Timeout')); - }, timeout); - } - - function cleanup(){ - script.parentNode.removeChild(script); - window[id] = noop; - } - - window[id] = function(data){ - debug('jsonp got', data); - if (timer) clearTimeout(timer); - cleanup(); - if (fn) fn(null, data); - }; - - // add qs component - url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id); - url = url.replace('?&', '?'); - - debug('jsonp req "%s"', url); - - // create script - script = document.createElement('script'); - script.src = url; - target.parentNode.insertBefore(script, target); -} - -}); -require.register("visionmedia-debug/debug.js", function(exports, require, module){ - -/** - * Expose `debug()` as the module. - */ - -module.exports = debug; - -/** - * Create a debugger with the given `name`. - * - * @param {String} name - * @return {Type} - * @api public - */ - -function debug(name) { - if (!debug.enabled(name)) return function(){}; - - return function(fmt){ - fmt = coerce(fmt); - - var curr = new Date; - var ms = curr - (debug[name] || curr); - debug[name] = curr; - - fmt = name - + ' ' - + fmt - + ' +' + debug.humanize(ms); - - // This hackery is required for IE8 - // where `console.log` doesn't have 'apply' - window.console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); - } -} - -/** - * The currently active debug mode names. - */ - -debug.names = []; -debug.skips = []; - -/** - * Enables a debug mode by name. This can include modes - * separated by a colon and wildcards. - * - * @param {String} name - * @api public - */ - -debug.enable = function(name) { - try { - localStorage.debug = name; - } catch(e){} - - var split = (name || '').split(/[\s,]+/) - , len = split.length; - - for (var i = 0; i < len; i++) { - name = split[i].replace('*', '.*?'); - if (name[0] === '-') { - debug.skips.push(new RegExp('^' + name.substr(1) + '$')); - } - else { - debug.names.push(new RegExp('^' + name + '$')); - } - } -}; - -/** - * Disable debug output. - * - * @api public - */ - -debug.disable = function(){ - debug.enable(''); -}; - -/** - * Humanize the given `ms`. - * - * @param {Number} m - * @return {String} - * @api private - */ - -debug.humanize = function(ms) { - var sec = 1000 - , min = 60 * 1000 - , hour = 60 * min; - - if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; - if (ms >= min) return (ms / min).toFixed(1) + 'm'; - if (ms >= sec) return (ms / sec | 0) + 's'; - return ms + 'ms'; -}; - -/** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - -debug.enabled = function(name) { - for (var i = 0, len = debug.skips.length; i < len; i++) { - if (debug.skips[i].test(name)) { - return false; - } - } - for (var i = 0, len = debug.names.length; i < len; i++) { - if (debug.names[i].test(name)) { - return true; - } - } - return false; -}; - -/** - * Coerce `val`. - */ - -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; -} - -// persist - -try { - if (window.localStorage) debug.enable(localStorage.debug); -} catch(e){} - -}); -require.register("johntron-asap/asap.js", function(exports, require, module){ -"use strict"; - -// Use the fastest possible means to execute a task in a future turn -// of the event loop. - -// linked list of tasks (single, with head node) -var head = {task: void 0, next: null}; -var tail = head; -var flushing = false; -var requestFlush = void 0; -var hasSetImmediate = typeof setImmediate === "function"; -var domain; - -if (typeof global != 'undefined') { - // Avoid shims from browserify. - // The existence of `global` in browsers is guaranteed by browserify. - var process = global.process; -} - -// Note that some fake-Node environments, -// like the Mocha test runner, introduce a `process` global. -var isNodeJS = !!process && ({}).toString.call(process) === "[object process]"; - -function flush() { - /* jshint loopfunc: true */ - - while (head.next) { - head = head.next; - var task = head.task; - head.task = void 0; - - try { - task(); - - } catch (e) { - if (isNodeJS) { - // In node, uncaught exceptions are considered fatal errors. - // Re-throw them to interrupt flushing! - - // Ensure continuation if an uncaught exception is suppressed - // listening process.on("uncaughtException") or domain("error"). - requestFlush(); - - throw e; - - } else { - // In browsers, uncaught exceptions are not fatal. - // Re-throw them asynchronously to avoid slow-downs. - throw e; - } - } - } - - flushing = false; -} - -if (isNodeJS) { - // Node.js - requestFlush = function () { - // Ensure flushing is not bound to any domain. - var currentDomain = process.domain; - if (currentDomain) { - domain = domain || (1,require)("domain"); - domain.active = process.domain = null; - } - - // Avoid tick recursion - use setImmediate if it exists. - if (flushing && hasSetImmediate) { - setImmediate(flush); - } else { - process.nextTick(flush); - } - - if (currentDomain) { - domain.active = process.domain = currentDomain; - } - }; - -} else if (hasSetImmediate) { - // In IE10, or https://github.com/NobleJS/setImmediate - requestFlush = function () { - setImmediate(flush); - }; - -} else if (typeof MessageChannel !== "undefined") { - // modern browsers - // http://www.nonblocking.io/2011/06/windownexttick.html - var channel = new MessageChannel(); - // At least Safari Version 6.0.5 (8536.30.1) intermittently cannot create - // working message ports the first time a page loads. - channel.port1.onmessage = function () { - requestFlush = requestPortFlush; - channel.port1.onmessage = flush; - flush(); - }; - var requestPortFlush = function () { - // Opera requires us to provide a message payload, regardless of - // whether we use it. - channel.port2.postMessage(0); - }; - requestFlush = function () { - setTimeout(flush, 0); - requestPortFlush(); - }; - -} else { - // old browsers - requestFlush = function () { - setTimeout(flush, 0); - }; -} - -function asap(task) { - if (isNodeJS && process.domain) { - task = process.domain.bind(task); - } - - tail = tail.next = {task: task, next: null}; - - if (!flushing) { - requestFlush(); - flushing = true; - } -}; - -module.exports = asap; - -}); -require.register("chrissrogers-promise/index.js", function(exports, require, module){ -'use strict'; - -//This file contains then/promise specific extensions to the core promise API - -var Promise = require('./core.js') -var asap = require('asap') - -module.exports = Promise - -/* Static Functions */ - -function ValuePromise(value) { - this.then = function (onFulfilled) { - if (typeof onFulfilled !== 'function') return this - return new Promise(function (resolve, reject) { - asap(function () { - try { - resolve(onFulfilled(value)) - } catch (ex) { - reject(ex); - } - }) - }) - } -} -ValuePromise.prototype = Promise.prototype - -var TRUE = new ValuePromise(true) -var FALSE = new ValuePromise(false) -var NULL = new ValuePromise(null) -var UNDEFINED = new ValuePromise(undefined) -var ZERO = new ValuePromise(0) -var EMPTYSTRING = new ValuePromise('') - -Promise.resolve = function (value) { - if (value instanceof Promise) return value - - if (value === null) return NULL - if (value === undefined) return UNDEFINED - if (value === true) return TRUE - if (value === false) return FALSE - if (value === 0) return ZERO - if (value === '') return EMPTYSTRING - - if (typeof value === 'object' || typeof value === 'function') { - try { - var then = value.then - if (typeof then === 'function') { - return new Promise(then.bind(value)) - } - } catch (ex) { - return new Promise(function (resolve, reject) { - reject(ex) - }) - } - } - - return new ValuePromise(value) -} - -Promise.from = Promise.cast = function (value) { - var err = new Error('Promise.from and Promise.cast are deprecated, use Promise.resolve instead') - err.name = 'Warning' - console.warn(err.stack) - return Promise.resolve(value) -} - -Promise.denodeify = function (fn, argumentCount) { - argumentCount = argumentCount || Infinity - return function () { - var self = this - var args = Array.prototype.slice.call(arguments) - return new Promise(function (resolve, reject) { - while (args.length && args.length > argumentCount) { - args.pop() - } - args.push(function (err, res) { - if (err) reject(err) - else resolve(res) - }) - fn.apply(self, args) - }) - } -} -Promise.nodeify = function (fn) { - return function () { - var args = Array.prototype.slice.call(arguments) - var callback = typeof args[args.length - 1] === 'function' ? args.pop() : null - try { - return fn.apply(this, arguments).nodeify(callback) - } catch (ex) { - if (callback === null || typeof callback == 'undefined') { - return new Promise(function (resolve, reject) { reject(ex) }) - } else { - asap(function () { - callback(ex) - }) - } - } - } -} - -Promise.all = function () { - var calledWithArray = arguments.length === 1 && Array.isArray(arguments[0]) - var args = Array.prototype.slice.call(calledWithArray ? arguments[0] : arguments) - - if (!calledWithArray) { - var err = new Error('Promise.all should be called with a single array, calling it with multiple arguments is deprecated') - err.name = 'Warning' - console.warn(err.stack) - } - - return new Promise(function (resolve, reject) { - if (args.length === 0) return resolve([]) - var remaining = args.length - function res(i, val) { - try { - if (val && (typeof val === 'object' || typeof val === 'function')) { - var then = val.then - if (typeof then === 'function') { - then.call(val, function (val) { res(i, val) }, reject) - return - } - } - args[i] = val - if (--remaining === 0) { - resolve(args); - } - } catch (ex) { - reject(ex) - } - } - for (var i = 0; i < args.length; i++) { - res(i, args[i]) - } - }) -} - -Promise.reject = function (value) { - return new Promise(function (resolve, reject) { - reject(value); - }); -} - -Promise.race = function (values) { - return new Promise(function (resolve, reject) { - values.forEach(function(value){ - Promise.resolve(value).then(resolve, reject); - }) - }); -} - -/* Prototype Methods */ - -Promise.prototype.done = function (onFulfilled, onRejected) { - var self = arguments.length ? this.then.apply(this, arguments) : this - self.then(null, function (err) { - asap(function () { - throw err - }) - }) -} - -Promise.prototype.nodeify = function (callback) { - if (typeof callback != 'function') return this - - this.then(function (value) { - asap(function () { - callback(null, value) - }) - }, function (err) { - asap(function () { - callback(err) - }) - }) -} - -Promise.prototype['catch'] = function (onRejected) { - return this.then(null, onRejected); -} - -}); -require.register("chrissrogers-promise/core.js", function(exports, require, module){ -'use strict'; - -var asap = require('asap') - -module.exports = Promise -function Promise(fn) { - if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new') - if (typeof fn !== 'function') throw new TypeError('not a function') - var state = null - var value = null - var deferreds = [] - var self = this - - this.then = function(onFulfilled, onRejected) { - return new self.constructor(function(resolve, reject) { - handle(new Handler(onFulfilled, onRejected, resolve, reject)) - }) - } - - function handle(deferred) { - if (state === null) { - deferreds.push(deferred) - return - } - asap(function() { - var cb = state ? deferred.onFulfilled : deferred.onRejected - if (cb === null) { - (state ? deferred.resolve : deferred.reject)(value) - return - } - var ret - try { - ret = cb(value) - } - catch (e) { - deferred.reject(e) - return - } - deferred.resolve(ret) - }) - } - - function resolve(newValue) { - try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure - if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.') - if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { - var then = newValue.then - if (typeof then === 'function') { - doResolve(then.bind(newValue), resolve, reject) - return - } - } - state = true - value = newValue - finale() - } catch (e) { reject(e) } - } - - function reject(newValue) { - state = false - value = newValue - finale() - } - - function finale() { - for (var i = 0, len = deferreds.length; i < len; i++) - handle(deferreds[i]) - deferreds = null - } - - doResolve(fn, resolve, reject) -} - - -function Handler(onFulfilled, onRejected, resolve, reject){ - this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null - this.onRejected = typeof onRejected === 'function' ? onRejected : null - this.resolve = resolve - this.reject = reject -} - -/** - * Take a potentially misbehaving resolver function and make sure - * onFulfilled and onRejected are only called once. - * - * Makes no guarantees about asynchrony. - */ -function doResolve(fn, onFulfilled, onRejected) { - var done = false; - try { - fn(function (value) { - if (done) return - done = true - onFulfilled(value) - }, function (reason) { - if (done) return - done = true - onRejected(reason) - }) - } catch (ex) { - if (done) return - done = true - onRejected(ex) - } -} - -}); -require.register("kewah-mixin/index.js", function(exports, require, module){ -if (typeof Object.keys === 'function') { - module.exports = function(to, from) { - Object.keys(from).forEach(function(property) { - Object.defineProperty(to, property, Object.getOwnPropertyDescriptor(from, property)); - }); - }; -} else { - module.exports = function(to, from) { - for (var property in from) { - if (from.hasOwnProperty(property)) { - to[property] = from[property]; - } - } - }; -} - -}); -require.register("pluma-par/dist/par.js", function(exports, require, module){ -/*! par 0.3.0 Original author Alan Plum . Released into the Public Domain under the UNLICENSE. @preserve */ -var slice = Array.prototype.slice; - -function par(fn) { - var args0 = slice.call(arguments, 1); - return function() { - var argsN = slice.call(arguments, 0), - args = []; - args.push.apply(args, args0); - args.push.apply(args, argsN); - return fn.apply(this, args); - }; -} - -function rpartial(fn) { - var argsN = slice.call(arguments, 1); - return function() { - var args = slice.call(arguments, 0); - args.push.apply(args, argsN); - return fn.apply(this, args); - }; -} - -par.rpartial = rpartial; -par.lpartial = par; - -module.exports = par; - -}); -require.register("ianstormtaylor-to-no-case/index.js", function(exports, require, module){ - -/** - * Expose `toNoCase`. - */ - -module.exports = toNoCase; - - -/** - * Test whether a string is camel-case. - */ - -var hasSpace = /\s/; -var hasCamel = /[a-z][A-Z]/; -var hasSeparator = /[\W_]/; - - -/** - * Remove any starting case from a `string`, like camel or snake, but keep - * spaces and punctuation that may be important otherwise. - * - * @param {String} string - * @return {String} - */ - -function toNoCase (string) { - if (hasSpace.test(string)) return string.toLowerCase(); - - if (hasSeparator.test(string)) string = unseparate(string); - if (hasCamel.test(string)) string = uncamelize(string); - return string.toLowerCase(); -} - - -/** - * Separator splitter. - */ - -var separatorSplitter = /[\W_]+(.|$)/g; - - -/** - * Un-separate a `string`. - * - * @param {String} string - * @return {String} - */ - -function unseparate (string) { - return string.replace(separatorSplitter, function (m, next) { - return next ? ' ' + next : ''; - }); -} - - -/** - * Camelcase splitter. - */ - -var camelSplitter = /(.)([A-Z]+)/g; - - -/** - * Un-camelcase a `string`. - * - * @param {String} string - * @return {String} - */ - -function uncamelize (string) { - return string.replace(camelSplitter, function (m, previous, uppers) { - return previous + ' ' + uppers.toLowerCase().split('').join(' '); - }); -} -}); -require.register("ianstormtaylor-to-space-case/index.js", function(exports, require, module){ - -var clean = require('to-no-case'); - - -/** - * Expose `toSpaceCase`. - */ - -module.exports = toSpaceCase; - - -/** - * Convert a `string` to space case. - * - * @param {String} string - * @return {String} - */ - - -function toSpaceCase (string) { - return clean(string).replace(/[\W_]+(.|$)/g, function (matches, match) { - return match ? ' ' + match : ''; - }); -} -}); -require.register("ianstormtaylor-to-slug-case/index.js", function(exports, require, module){ - -var toSpace = require('to-space-case'); - - -/** - * Expose `toSlugCase`. - */ - -module.exports = toSlugCase; - - -/** - * Convert a `string` to slug case. - * - * @param {String} string - * @return {String} - */ - - -function toSlugCase (string) { - return toSpace(string).replace(/\s/g, '-'); -} -}); -require.register("recurly/lib/index.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var Recurly = require('./recurly'); - -/** - * Export a single instance. - */ - -module.exports = exports = new Recurly(); - -/** - * Hack for testing. - */ - -exports.Recurly = Recurly; - -}); -require.register("recurly/lib/recurly.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var bind = require('bind'); -var json = require('json'); -var each = require('each'); -var type = require('type'); -var merge = require('merge'); -var mixin = require('mixin'); -var jsonp = require('jsonp'); -var qs = require('querystring'); -var Emitter = require('emitter'); -var errors = require('./errors'); -var version = require('./version'); -var debug = require('debug')('recurly'); - -/** - * Default configuration values. - * - * @private - * @type {Object} - */ - -var defaults = { - currency: 'USD' - , timeout: 60000 - , publicKey: '' - , api: 'https://api.recurly.com/js/v1' -}; - -/** - * API mixins. - * - * @type {Array} - * @private - */ - -var mixins = [ - 'open' - , 'coupon' - , 'paypal' - , 'plan' - , 'tax' - , 'token' - , 'pricing' - , 'validate' -]; - -/** - * Export `Recurly`. - */ - -module.exports = Recurly; - -/** - * Initialize defaults. - * - * @param {Object} options - * @constructor - * @public - */ - -function Recurly (options) { - this.id = 0; - this.version = version; - this.configured = false; - this.config = merge({}, defaults); - if (options) this.configure(options); -} - -/** - * Inherits `Emitter`. - */ - -Emitter(Recurly.prototype); - -/** - * Configure settings. - * - * @param {String|Object} options Either publicKey or object containing - * publicKey and other optional members - * @param {String} options.publicKey - * @param {String} [options.currency] - * @param {String} [options.api] - * @public - */ - -Recurly.prototype.configure = function configure (options) { - if (this.configured) throw errors('already-configured'); - - debug('configure'); - - if (type(options) === 'string') options = { publicKey: options }; - - if ('publicKey' in options) { - this.config.publicKey = options.publicKey; - } else { - throw errors('missing-public-key'); - } - - if ('api' in options) { - this.config.api = options.api; - } - - if ('currency' in options) { - this.config.currency = options.currency; - } - - this.configured = true; -}; - -/** - * Assembles the API endpoint. - * - * @return {String} route - * @private - */ - -Recurly.prototype.url = function url (route) { - return this.config.api + route; -}; - -/** - * Issues an API request. - * - * @param {String} route - * @param {Object} [data] - * @param {Function} done - * @throws {Error} If `configure` has not been called. - * @private - */ - -Recurly.prototype.request = function request (route, data, done) { - debug('request'); - - if (false === this.configured) { - throw errors('not-configured'); - } - - if ('function' == type(data)) { - done = data; - data = {}; - } - - var url = this.url(route); - var timeout = this.config.timeout; - - data.version = this.version; - data.key = this.config.publicKey; - - url += '?' + qs.stringify(data); - - this.cache(url, function (res, set) { - if (res) return done(null, res); - jsonp(url, { timeout: timeout }, function (err, res) { - if (err) return done(err); - if (res.error) { - done(errors('api-error', res.error)); - } else { - done(null, set(res)); - } - }); - }); -}; - -/** - * Caches an object - * - * TODO: figure out invalidation & expiry - * - * @param {String} url - * @param {Function} done - * @private - */ - -Recurly.prototype.cache = function cache (url, done) { - debug('cache'); - var stored = localStorage.getItem(url); - if (stored) { - debug('cache found ' + url); - return done(json.parse(stored)); - } else { - debug('cache set ' + url); - return done(null, set); - } - function set (obj) { - // disabled for now - // localStorage.setItem(url, json.stringify(obj)); - return obj; - } -}; - -/** - * Load the `mixins` onto Recurly.prototype. - */ - -each(mixins, function (name) { - mixin(Recurly.prototype, require('./recurly/' + name)); -}); - -}); -require.register("recurly/lib/version.js", function(exports, require, module){ - -/** - * Current package/component version. - */ - -module.exports = '3.0.5'; - -}); -require.register("recurly/lib/errors.js", function(exports, require, module){ -/** - * dependencies - */ - -var mixin = require('mixin'); - -/** - * Export `errors`. - */ - -module.exports = exports = errors; - -/** - * Error accessor. - * - * @param {String} name - * @param {Object} options - * @return {Error} - */ - -function errors (name, options) { - return errors.get(name, options); -} - -/** - * Defined errors. - * - * @type {Object} - * @private - */ - -errors.map = {}; - -/** - * Base url for documention. - * - * @type {String} - * @private - */ - -errors.baseURL = ''; - -/** - * Sets the `baseURL` for docs. - * - * @param {String} url - * @public - */ - -errors.doc = function (baseURL) { - errors.baseURL = baseURL; -}; - -/** - * Gets errors defined by `name`. - * - * @param {String} name - * @param {Object} context - * @return {Error} - * @public - */ - -errors.get = function (name, context) { - if (!(name in errors.map)) { - throw new Error('invalid error'); - } else { - return new errors.map[name](context); - } -}; - -/** - * Registers an error defined by `name` with `config`. - * - * @param {String} name - * @param {Object} config - * @return {Error} - * @public - */ - -errors.add = function (name, config) { - config = config || {}; - - function RecurlyError (context) { - Error.call(this); - - this.name = this.code = name; - this.message = config.message; - mixin(this, context || {}); - - if (config.help) { - this.help = errors.baseURL + config.help; - this.message += ' (need help? ' + this.help + ')'; - } - }; - - RecurlyError.prototype = new Error(); - return errors.map[name] = RecurlyError; -}; - -/** - * Internal definations. - * - * TODO(gjohnson): open source this as a component - * and move these out. - */ - -errors.doc('https://docs.recurly.com/js'); - -errors.add('already-configured', { - message: 'Configuration may only be set once.', - help: '#identify-your-site' -}); - -errors.add('not-configured', { - message: 'Not configured. You must first call recurly.configure().', - help: '#identify-your-site' -}); - -errors.add('missing-public-key', { - message: 'The publicKey setting is required.', - help: '#identify-your-site' -}); - -errors.add('api-error', { - message: 'There was an error with your request.' -}); - -errors.add('validation', { - message: 'There was an error validating your request.' -}); - -errors.add('missing-callback', { - message: 'Missing callback' -}); - -errors.add('invalid-options', { - message: 'Options must be an object' -}); - -errors.add('missing-plan', { - message: 'A plan must be specified.' -}); - -errors.add('missing-coupon', { - message: 'A coupon must be specified.' -}); - -errors.add('invalid-item', { - message: 'The given item does not appear to be a valid recurly plan, coupon, addon, or taxable address.' -}); - -errors.add('invalid-addon', { - message: 'The given addon_code is not among the valid addons for the specified plan.' -}); - -errors.add('invalid-currency', { - message: 'The given currency is not among the valid codes for the specified plan.' -}); - -errors.add('unremovable-item', { - message: 'The given item cannot be removed.' -}); - -}); -require.register("recurly/lib/util/dom.js", function(exports, require, module){ -/** - * dependencies - */ - -var slug = require('to-slug-case'); -var type = require('type'); -var each = require('each'); -var map = require('map'); - -/** - * expose - */ - -module.exports = { - element: element, - value: value, - data: data -}; - -/** - * Detects whether an object is an html element. - * - * @param {Mixed} node - * @return {HTMLElement|Boolean} node - */ - -function element (node) { - var isJQuery = window.jQuery && node instanceof jQuery; - var isArray = type(node) === 'array'; - if (isJQuery || isArray) node = node[0]; - - var isElem = typeof HTMLElement !== 'undefined' - ? node instanceof HTMLElement - : node && node.nodeType === 1; - - return isElem && node; -}; - -/** - * Gets or sets the value of a given HTML form element - * - * supports text inputs, radio inputs, and selects - * - * @param {HTMLElement} node - * @return {String} value of the element - */ - -function value (node, value) { - if (!element(node)) return null; - return typeof value !== 'undefined' - ? valueSet(node, value) - : valueGet(node); -} - -/** - * Gets an HTMLElement's value property in the context of a form - * - * @param {HTMLElement} node - * @return {String} node's value - */ - -function valueGet (node) { - node = element(node); - - var nodeType = node && node.type && node.type.toLowerCase(); - var value; - - if (!nodeType) { - value = ''; - } else if ('options' in node) { - value = node.options[node.selectedIndex].value; - } else if (nodeType === 'checkbox') { - if (node.checked) value = node.value; - } else if (nodeType === 'radio') { - var radios = document.querySelectorAll('input[data-recurly="' + data(node, 'recurly') + '"]'); - each(radios, function (radio) { - if (radio.checked) value = radio.value; - }); - } else if ('value' in node) { - value = node.value; - } - - return value; -} - -/** - * Updates an element's value property if - * one exists; else innerText if it exists - * - * @param {Array[HTMLElement]} nodes - * @param {Mixed} value - */ - -function valueSet (nodes, value) { - if (type(nodes) !== 'array') nodes = [nodes]; - each(nodes, function (node) { - if (!node) return; - else if ('value' in node) - node.value = value; - else if ('textContent' in node) - node.textContent = value; - else if ('innerText' in node) - node.innerText = value; - }); -} - -/** - * Gets or sets a node's data attribute - * - * @param {HTMLElement} node - * @param {String} key - * @param {Mixed} [value] - */ - -function data (node, key, value) { - node = element(node); - if (!node) return; - return typeof value !== 'undefined' - ? dataSet(node, key, value) - : dataGet(node, key); -} - -/** - * Gets a node's data attribute - * - * @param {HTMLElement} node - * @param {String} key - */ - -function dataGet (node, key) { - return node.dataset - ? node.dataset[key] - : node.getAttribute('data-' + slug(key)); -} - -/** - * sets a node's data attribute - * - * @param {HTMLElement} node - * @param {String} key - * @param {Mixed} value - */ - -function dataSet (node, key, value) { - if (node.dataset) node.dataset[key] = value; - else node.setAttribute('data-' + slug(key), value); -} - -}); -require.register("recurly/lib/util/parse-card.js", function(exports, require, module){ - -/** - * Removes dashes and spaces from a card number. - * - * @param {Number|String} number - * @return {String} parsed card number - */ - -module.exports = function parseCard (number) { - return number && number.toString().replace(/[-\s]/g, ''); -}; - -}); -require.register("recurly/lib/recurly/open.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var bind = require('bind'); -var type = require('type'); -var json = require('json'); -var events = require('event'); -var qs = require('querystring'); -var errors = require('../errors'); -var debug = require('debug')('recurly:open'); - -/** - * Issues an API request to a popup window. - * - * TODO(*): configurable window name? - * TODO(*): configurable window properties? - * - * @param {String} url - * @param {Object} [data] - * @param {Function} [done] - * @throws {Error} If `configure` has not been called. - * @return {Window} - * @private - */ - -exports.open = function (url, data, done) { - debug('open'); - - if (false === this.configured) { - throw errors('not-configured'); - } - - if ('function' == type(data)) { - done = data; - data = {}; - } - - data = data || {}; - data.version = this.version; - data.event = 'recurly-open-' + this.id++; - data.key = this.config.publicKey; - this.once(data.event, done); - - if (!/^https?:\/\//.test(url)) url = this.url(url); - url += (~url.indexOf('?') ? '&' : '?') + qs.stringify(data); - - this.relay(function () { - window.open(url); - }); -}; - -/** - * Relay mixin. - * - * Inspects the window for intent to relay a message, - * then attempts to send it off. closes the window once - * dispatched. - * - * @param {Function} done - * @private - */ - -exports.relay = function (done) { - var self = this; - - if (false === this.configured) { - throw errors('not-configured'); - } - - events.bind(window, 'message', function listener (event) { - var data = json.parse(event.data); - var name = data.recurly_event; - var body = data.recurly_message; - var err = body.error ? errors('api-error', body.error) : null; - events.unbind(window, 'message', listener); - if (name) self.emit(name, err, body); - if (frame) document.body.removeChild(frame); - }); - - if ('documentMode' in document) { - var frame = document.createElement('iframe'); - frame.width = frame.height = 0; - frame.src = this.url('/relay'); - frame.name = 'recurly-relay'; - frame.style.display = 'none'; - frame.onload = bind(this, done); - document.body.appendChild(frame); - } else { - done(); - } -}; - -}); -require.register("recurly/lib/recurly/coupon.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var type = require('type'); -var debug = require('debug')('recurly:coupon'); -var errors = require('../errors'); - -/** - * Coupon mixin. - * - * Retrieves coupon information for the `plan`. The `callback` signature - * is `err, plan` where `err` may be a request or server error, and `plan` - * is a representation of the requested plan. - * - * @param {Object} options - * @param {Function} callback - */ - -exports.coupon = function (options, callback) { - debug('%j', options); - - if ('function' !== type(callback)) { - throw errors('missing-callback'); - } - - if ('object' !== type(options)) { - throw errors('invalid-options'); - } - - if (!('plan' in options)) { - throw errors('missing-plan'); - } - - if (!('coupon' in options)) { - throw errors('missing-coupon'); - } - - this.request('/plans/' + options.plan + '/coupons/' + options.coupon, options, callback); -}; - -}); -require.register("recurly/lib/recurly/paypal.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var debug = require('debug')('recurly:paypal'); - -/** - * Paypal mixin. - * - * @param {Object} data - * @param {Function} done callback - */ - -exports.paypal = function (data, done) { - debug('start'); - this.open('/paypal/start', data, done); -}; - -}); -require.register("recurly/lib/recurly/plan.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var type = require('type'); -var debug = require('debug')('recurly:plan'); - -/** - * Plan mixin. - * - * Retrieves information for the `plan`. The `callback` signature - * is `err, plan` where `err` may be a request or server error, and `plan` - * is a representation of the requested plan. - * - * @param {String} code - * @param {Function} callback - */ - -exports.plan = function (code, callback) { - debug('%s', code); - - if ('function' != type(callback)) { - throw new Error('Missing callback'); - } - - if ('undefined' == type(code)) { - return callback(new Error('Missing plan code')); - } - - this.request('/plans/' + code, callback); -}; - -}); -require.register("recurly/lib/recurly/tax.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var type = require('type'); -var clone = require('clone'); -var debug = require('debug')('recurly:tax'); - -/** - * Tax mixin. - * - * Provides a tax estiamte for the given address. - * - * @param {Object} options - * @param {Object} options.postal_code - * @param {Object} options.country - * @param {Object} [options.vat_number] Used for VAT exemptions - * @param {Function} callback - */ - -exports.tax = function (options, callback) { - var request = clone(options); - - if ('function' != type(callback)) { - throw new Error('Missing callback'); - } - - if (!('currency' in request)) { - request.currency = this.config.currency; - } - - this.request('/tax', request, callback); -}; - -}); -require.register("recurly/lib/recurly/token.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var bind = require('bind'); -var each = require('each'); -var type = require('type'); -var index = require('indexof'); -var debug = require('debug')('recurly:token'); -var dom = require('../util/dom'); -var parseCard = require('../util/parse-card'); -var errors = require('../errors'); - -/** - * Fields that are sent to API. - * - * @type {Array} - * @private - */ - -var fields = [ - 'first_name' - , 'last_name' - , 'number' - , 'month' - , 'year' - , 'cvv' - , 'address1' - , 'address2' - , 'country' - , 'city' - , 'state' - , 'postal_code' - , 'phone' - , 'vat_number' - , 'token' -]; - -/** - * Generates a token from customer data. - * - * The callback signature: `err, response` where `err` is a - * connection, request, or server error, and `response` is the - * recurly service response. The generated token is accessed - * at `response.token`. - * - * @param {Object|HTMLFormElement} options Billing properties or an HTMLFormElement - * with children corresponding to billing properties via 'data-reurly' attributes. - * @param {String} options.first_name customer first name - * @param {String} options.last_name customer last name - * @param {String|Number} options.number card number - * @param {String|Number} options.month card expiration month - * @param {String|Number} options.year card expiration year - * @param {String|Number} options.cvv card verification value - * @param {String} [options.address1] - * @param {String} [options.address2] - * @param {String} [options.country] - * @param {String} [options.city] - * @param {String} [options.state] - * @param {String|Number} [options.postal_code] - * @param {Function} done callback - */ - -exports.token = function (options, done) { - var open = bind(this, this.open); - var data = normalize(options); - var input = data.values; - var userErrors = validate.call(this, input); - - if ('function' !== type(done)) { - throw errors('missing-callback'); - } - - if (userErrors.length) { - return done(errors('validation', { fields: userErrors })); - } - - this.request('/token', input, function (err, res) { - if (err) return done(err); - if (data.fields.token && res.id) { - data.fields.token.value = res.id; - } - done(null, res); - }); -}; - -/** - * Parses options out of a form element and normalizes according to rules. - * - * @param {Object|HTMLFormElement} options - * @return {Object} - */ - -function normalize (options) { - var el = dom.element(options); - var data = { fields: {}, values: {} }; - - if (el && 'form' === el.nodeName.toLowerCase()) { - each(el.querySelectorAll('[data-recurly]'), function (field) { - var name = dom.data(field, 'recurly'); - if (~index(fields, name)) { - data.fields[name] = field; - data.values[name] = dom.value(field); - } - }); - } else { - data.values = options; - } - - data.values.number = parseCard(data.values.number); - - return data; -} - -/** - * Checks user input on a token call - * - * @param {Object} input - * @return {Array} indicates which fields are not valid - */ - -function validate (input) { - var errors = []; - - if (!this.validate.cardNumber(input.number)) { - errors.push('number'); - } - - if (!this.validate.expiry(input.month, input.year)) { - errors.push('month', 'year'); - } - - if (!input.first_name) { - errors.push('first_name'); - } - - if (!input.last_name) { - errors.push('last_name'); - } - - return errors; -} - -}); -require.register("recurly/lib/recurly/validate.js", function(exports, require, module){ - -/*! - * Module dependencies. - */ - -var find = require('find'); -var trim = require('trim'); -var index = require('indexof'); -var parseCard = require('../util/parse-card'); - -/** - * Card patterns. - * - * @private - */ - -var types = [ - { - type: 'discover', - pattern: /^(6011|622|64[4-9]|65)/, - lengths: [16] - } - , { - type: 'master', - pattern: /^5[0-5]/, - lengths: [16] - } - , { - type: 'american_express', - pattern: /^3[47]/, - lengths: [15] - } - , { - type: 'visa', - pattern: /^4/, - lengths: [13, 16] - } - , { - type: 'jcb', - pattern: /^35[2-8]\d/, - lengths: [16] - } - , { - type: 'diners_club', - pattern: /^(30[0-5]|309|36|3[89]|54|55|2014|2149)/, - lengths: [14] - } -]; - -/** - * Validate mixin. - * - * @public - */ - -exports.validate = { - - /** - * Validates a credit card number via luhn algorithm. - * - * @param {Number|String} number The card number. - * @return {Boolean} - * @see https://sites.google.com/site/abapexamples/javascript/luhn-validation - */ - - cardNumber: function (number) { - var str = parseCard(number); - var ca, sum = 0, mul = 1; - var i = str.length; - - while (i--) { - ca = parseInt(str.charAt(i), 10) * mul; - sum += ca - (ca > 9) * 9; - mul ^= 3; - } - - return sum % 10 === 0 && sum > 0; - }, - - /** - * Returns the type of the card number as a string. - * - * TODO(chrissrogers): Maybe undefined instread of "unknown"? - * - * @param {Number|String} number The card number - * @return {String} card type - */ - - cardType: function (number) { - var str = parseCard(number); - var card = find(types, function (card) { - return card.pattern.test(str) && ~index(card.lengths, str.length); - }); - return card && card.type || 'unknown'; - }, - - /** - * Validates whether an expiry month is present or future. - * - * @param {Numer|String} month The 2 digit month - * @param {Numer|String} year The 2 or 4 digit year - * @return {Boolean} - */ - - expiry: function (month, year) { - month = parseInt(month, 10) - 1; - if (month < 0 || month > 11) return false; - year = parseInt(year, 10); - year += year < 100 ? 2000 : 0; - - var expiry = new Date; - expiry.setYear(year); - expiry.setDate(1); - expiry.setHours(0); - expiry.setMinutes(0); - expiry.setSeconds(0); - expiry.setMonth(month + 1); - return new Date < expiry; - }, - - /** - * Validates whether a number looks like a cvv. - * - * e.g.: '123', '0321' - * - * @param {Number|String} number The card verification value - * @return {Boolean} - */ - - cvv: function (number) { - number = trim(number + ''); - return /^\d+$/.test(number) && (number.length === 3 || number.length === 4); - } - -}; - -}); -require.register("recurly/lib/recurly/pricing/index.js", function(exports, require, module){ -/** - * dependencies - */ - -var Emitter = require('emitter'); -var index = require('indexof'); -var each = require('each'); -var type = require('type'); -var bind = require('bind'); -var find = require('find'); -var mixin = require('mixin'); -var keys = require('object').keys; -var json = require('json'); -var debug = require('debug')('recurly:pricing'); -var PricingPromise = require('./promise'); -var Calculations = require('./calculations'); -var errors = require('../../errors'); - -/** - * expose - */ - -exports.Pricing = Pricing; - -/** - * Pricing - * - * @constructor - * @param {Recurly} recurly - * @public - */ - -function Pricing (recurly) { - if (this instanceof require('../../recurly')) return new Pricing(this); - this.recurly = recurly; - this.reset(); -} - -Emitter(Pricing.prototype); - -/** - * Subscription properties - */ - -Pricing.properties = [ - 'plan' - , 'addon' - , 'coupon' - , 'address' - , 'currency' -]; - -/** - * Resets the pricing calculator - * - * @public - */ - -Pricing.prototype.reset = function () { - this.items = {}; - this.items.addons = []; - this.currency(this.recurly.config.currency); -}; - -/** - * Removes an object from the pricing model - * - * example - * - * .remove({ plan: 'plan_code' }); - * .remove({ addon: 'addon_code' }); - * .remove({ coupon: 'coupon_code' }); - * .remove({ address: true }); // to remove without specifying a code - * - * @param {Object} opts - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.remove = function (opts, done) { - var self = this; - var item; - debug('remove'); - - return new PricingPromise(function (resolve, reject) { - var prop = keys(opts)[0]; - var id = opts[prop]; - if (!~index(Pricing.properties, prop)) return reject(errors('invalid-item')); - if (prop === 'addon') { - var pos = index(self.items.addons, findAddon(self.items.addons, { code: id })); - if (~pos) { - item = self.items.addons.splice(pos); - } - } else if (self.items[prop] && (id === self.items[prop].code || id === true)) { - item = self.items[prop] - delete self.items[prop]; - } else { - return reject(errors('unremovable-item', { - type: prop - , id: id - , reason: 'does not exist on this pricing instance.' - })); - } - }, this).nodeify(done); -}; - -/** - * Provides a subscription price estimate using current state - * - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.reprice = function (done) { - var self = this; - debug('reprice'); - - return new PricingPromise(function (resolve, reject) { - if (!self.items.plan) return reject(errors('missing-plan')); - - Calculations(self, function (price) { - if (json.stringify(price) === json.stringify(self.price)) return resolve(price); - self.price = price; - self.emit('change', price); - resolve(price); - }); - }, this).nodeify(done); -}; - -/** - * Updates plan - * - * @param {String} planCode - * @param {Object} [meta] - * @param {Number} [meta.quantity] - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.plan = function (planCode, meta, done) { - var self = this; - var plan = this.items.plan; - var quantity; - - if (type(meta) === 'function') { - done = meta; - meta = undefined; - } - - meta = meta || {}; - - // meta.quantity, plan.quantity, 1 - if (plan && plan.quantity) quantity = plan.quantity; - if (meta.quantity) quantity = parseInt(meta.quantity, 10); - if (!quantity || quantity < 1) quantity = 1; - - return new PricingPromise(function (resolve, reject) { - if (plan && plan.code === planCode) { - plan.quantity = quantity; - return resolve(plan); - } - - self.recurly.plan(planCode, function (err, plan) { - if (err) return reject(err); - - plan.quantity = quantity; - self.items.plan = plan; - - if (!(self.items.currency in plan.price)) { - self.currency(keys(plan.price)[0]); - } - - debug('set.plan'); - self.emit('set.plan', plan); - resolve(plan); - }); - }, this).nodeify(done); -}; - -/** - * Updates addon - * - * @param {String} addonCode - * @param {Object} [meta] - * @param {Number} [meta.quantity] - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.addon = function (addonCode, meta, done) { - var self = this; - - if (type(meta) === 'function') { - done = meta; - meta = undefined; - } - - meta = meta || {}; - - return new PricingPromise(function (resolve, reject) { - if (!self.items.plan) return reject(errors('missing-plan')); - - var planAddon = findAddon(self.items.plan.addons, addonCode); - if (!planAddon) { - return reject(errors('invalid-addon', { - planCode: self.items.plan.code - , addonCode: addonCode - })); - } - - var quantity = addonQuantity(meta, planAddon); - var addon = findAddon(self.items.addons, addonCode); - - if (quantity === 0) { - self.remove({ addon: addonCode }); - } - - if (addon) { - addon.quantity = quantity; - } else { - addon = json.parse(json.stringify(planAddon)); - addon.quantity = quantity; - self.items.addons.push(addon); - } - - debug('set.addon'); - self.emit('set.addon', addon); - resolve(addon); - }, this).nodeify(done); -}; - -/** - * Updates coupon - * - * @param {String} couponCode - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.coupon = function (couponCode, done) { - var self = this; - var coupon = this.items.coupon; - - return new PricingPromise(function (resolve, reject) { - if (!self.items.plan) return reject(errors('missing-plan')); - if (coupon) { - if (coupon.code === couponCode) return resolve(coupon); - else self.remove({ coupon: coupon.code }); - } - if (!couponCode) return resolve(); - - self.recurly.coupon({ plan: self.items.plan.code, coupon: couponCode }, function (err, coupon) { - if (err && err.code !== 'not_found') return reject(err); - - self.items.coupon = coupon; - - debug('set.coupon'); - self.emit('set.coupon', coupon); - resolve(coupon); - }); - }, this).nodeify(done); -}; - -/** - * Updates address - * - * @param {Object} address - * @param {String} address.country - * @param {String|Number} address.postal_code - * @param {String} address.vat_number - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.address = function (address, done) { - var self = this; - - return new PricingPromise(function (resolve, reject) { - if (json.stringify(address) === json.stringify(self.items.address)) { - return resolve(self.items.address); - } - - self.items.address = address; - - debug('set.address'); - self.emit('set.address', address); - resolve(address); - }, this).nodeify(done); -}; - -/** - * Updates or retrieves currency code - * - * @param {String} code - * @param {Function} [done] callback - * @public - */ - -Pricing.prototype.currency = function (code, done) { - var self = this; - var plan = this.items.plan - var currency = this.items.currency; - - return new PricingPromise(function (resolve, reject) { - if (currency === code) return resolve(currency); - if (plan && !(code in plan.price)) { - return reject(errors('invalid-currency', { - currencyCode: code - , planCurrencies: keys(plan.price) - })); - } - - self.items.currency = code; - - debug('set.currency'); - self.emit('set.currency', code); - resolve(code); - }, this).nodeify(done); -}; - -/** - * DOM attachment mixin - */ - -mixin(Pricing.prototype, require('./attach')); - -/** - * Utility functions - */ - -function addonQuantity (meta, planAddon) { - var qty = 1; - if ('quantity' in planAddon) qty = planAddon.quantity; - if ('quantity' in meta) qty = meta.quantity; - return parseInt(qty, 10) || 0; -} - -function findAddon (addons, code) { - return addons && find(addons, { code: code }); -} - -}); -require.register("recurly/lib/recurly/pricing/promise.js", function(exports, require, module){ -/** - * Dependencies - */ - -var Promise = require('promise'); -var mixin = require('mixin'); -var bind = require('bind'); -var each = require('each'); -var type = require('type'); -var par = require('par'); -var debug = require('debug')('recurly:pricing:promise'); - -/** - * Expose - */ - -module.exports = PricingPromise; - -/** - * PricingPromise - * - * issues repricing when .done - * - * contains .then wrappers for Pricing property methods - * - * Usage - * - * var pricing = recurly.Pricing(); - * - * pricing - * .plan('basic') - * .addon('addon1') - * .then(process) - * .catch(errors) - * .done(); - * - * @param {Function} resolver - * @param {Pricing} pricing bound instance - * @constructor - * @public - */ - -function PricingPromise (resolver, pricing) { - if (!(this instanceof PricingPromise)) return new PricingPromise(resolver, pricing); - - var self = this; - this.pricing = pricing; - this.constructor = par.rpartial(this.constructor, pricing); - - Promise.call(this, resolver); - - // for each pricing method, create a promise wrapper method - each(require('./').Pricing.prototype, function (method) { - self[method] = function () { - var args = arguments; - return self.then(function () { - return self.pricing[method].apply(self.pricing, args); - }); - }; - }); -} - -mixin(PricingPromise.prototype, Promise.prototype); -PricingPromise.prototype.constructor = PricingPromise; - -/** - * Adds a reprice and completes the control flow - * - * @param {Function} onFulfilled - * @param {Function} onRejected - * @return {Pricing} bound pricing instance - * @public - */ - -PricingPromise.prototype.done = function () { - Promise.prototype.done.apply(this.then(this.reprice), arguments); - return this.pricing; -}; - -/** - * Adds a reprice if a callback is passed - * - * @param {Function} [done] callback - * @public - */ - -PricingPromise.prototype.nodeify = function (done) { - if (type(done) === 'function') this.reprice(); - return Promise.prototype.nodeify.apply(this, arguments); -}; - -}); -require.register("recurly/lib/recurly/pricing/calculations.js", function(exports, require, module){ -/** - * dependencies - */ - -var each = require('each'); -var bind = require('bind'); -var find = require('find'); - -/** - * expose - */ - -module.exports = Calculations; - -/** - * Subscription calculation calculation - * - * @param {Pricing} pricing - * @constructor - * @public - */ - -function Calculations (pricing, done) { - if (!(this instanceof Calculations)) { - return new Calculations(pricing, done); - } - - this.pricing = pricing; - this.items = pricing.items; - - this.price = { - now: {}, - next: {}, - addons: {}, - currency: { - code: this.items.currency, - symbol: this.planPrice().symbol - } - }; - - this.subtotal(); - - this.tax(function () { - this.total(); - each(this.price.now, decimal, this.price.now); - each(this.price.next, decimal, this.price.next); - each(this.price.addons, decimal, this.price.addons); - done(this.price); - }); -} - -/** - * Calculates subtotal - * - * @private - */ - -Calculations.prototype.subtotal = function () { - var subtotal = this.planPrice().amount; - - this.price.now.subtotal = subtotal; - this.price.next.subtotal = subtotal; - - if (this.items.plan.trial) this.price.now.subtotal = 0; - - this.addons(); - this.price.now.subtotal += this.price.now.addons; - this.price.next.subtotal += this.price.next.addons; - - this.discount(); - this.price.now.subtotal -= this.price.now.discount; - this.price.next.subtotal -= this.price.next.discount; - - this.setupFee(); - this.price.now.subtotal += this.price.now.setup_fee; -}; - -/** - * Calculates tax - * - * @param {Function} done - * @private - */ - -Calculations.prototype.tax = function (done) { - this.price.now.tax = 0; - this.price.next.tax = 0; - - if (this.items.address) { - var self = this; - this.pricing.recurly.tax(this.items.address, function applyTax (err, taxes) { - if (err) { - self.pricing.emit('error', err); - } else { - each(taxes, function (tax) { - if (tax.type === 'usst' && self.items.plan.tax_exempt) return; - self.price.now.tax += self.price.now.subtotal * tax.rate; - self.price.next.tax += self.price.next.subtotal * tax.rate; - }); - - // tax estimation prefers partial cents to always round up - self.price.now.tax = Math.ceil(self.price.now.tax * 100) / 100; - self.price.next.tax = Math.ceil(self.price.next.tax * 100) / 100; - } - done.call(self); - }); - } else done.call(this); -}; - -/** - * Calculates total - * - * @private - */ - -Calculations.prototype.total = function () { - this.price.now.total = this.price.now.subtotal + this.price.now.tax; - this.price.next.total = this.price.next.subtotal + this.price.next.tax; -}; - -/** - * Computes addon prices and applies addons to the subtotal - * - * @private - */ - -Calculations.prototype.addons = function () { - this.price.now.addons = 0; - this.price.next.addons = 0; - - each(this.items.plan.addons, function (addon) { - var price = addon.price[this.items.currency].unit_amount; - - this.price.addons[addon.code] = price; - - var selected = find(this.items.addons, { code: addon.code }); - if (selected) { - price = price * selected.quantity; - if (!this.items.plan.trial) this.price.now.addons += price; - this.price.next.addons += price; - } - }, this); -}; - -/** - * Applies coupon discount to the subtotal - * - * @private - */ - -Calculations.prototype.discount = function () { - var coupon = this.items.coupon; - - this.price.now.discount = 0; - this.price.next.discount = 0; - - if (coupon) { - if (coupon.discount.rate) { - this.price.now.discount = Math.round(this.price.now.subtotal * coupon.discount.rate * 100) / 100; - this.price.next.discount = Math.round(this.price.next.subtotal * coupon.discount.rate * 100) / 100; - } else { - this.price.now.discount = coupon.discount.amount[this.items.currency]; - this.price.next.discount = coupon.discount.amount[this.items.currency]; - } - } -}; - -/** - * Applies plan setup fee to the subtotal - * - * @private - */ - -Calculations.prototype.setupFee = function () { - this.price.now.setup_fee = this.planPrice().setup_fee; - this.price.next.setup_fee = 0; -}; - -/** - * Get the price structure of a plan based on currency - * - * @return {Object} - * @private - */ - -Calculations.prototype.planPrice = function () { - var plan = this.items.plan; - var price = plan.price[this.items.currency]; - price.amount = price.unit_amount * (plan.quantity || 1); - return price; -}; - -/** - * Applies a decimal transform on an object's member - * - * @param {String} prop Property on {this} to transform - * @this {Object} on which to apply decimal transformation - * @private - */ - -function decimal (prop) { - this[prop] = (Math.round(Math.max(this[prop], 0) * 100) / 100).toFixed(2); -} - -}); -require.register("recurly/lib/recurly/pricing/attach.js", function(exports, require, module){ -/** - * dependencies - */ - -var each = require('each'); -var events = require('event'); -var find = require('find'); -var type = require('type'); -var dom = require('../../util/dom'); -var debug = require('debug')('recurly:pricing:attach'); - -/** - * bind a dom element to pricing values - * - * @param {HTMLElement} el - */ - -exports.attach = function (el) { - var self = this; - var elems = {}; - var el = dom.element(el); - - if (!el) throw new Error('invalid dom element'); - - if (this.attach.detatch) this.attach.detatch(); - - self.on('change', update); - - each(el.querySelectorAll('[data-recurly]'), function (elem) { - // 'zip' -> 'postal_code' - if (dom.data(elem, 'recurly') === 'zip') dom.data(elem, 'recurly', 'postal_code'); - - var name = dom.data(elem, 'recurly'); - if (!elems[name]) elems[name] = []; - elems[name].push(elem); - events.bind(elem, 'change', change); - events.bind(elem, 'propertychange', change); - }); - - this.attach.detatch = detatch; - - change(); - - function change (event) { - debug('change'); - - var targetName = event && event.target && dom.data(event.target, 'recurly'); - targetName = targetName || window.event && window.event.srcElement; - - var pricing = self.plan(dom.value(elems.plan), { quantity: dom.value(elems.plan_quantity) }); - - if (target('currency')) { - pricing = pricing.currency(dom.value(elems.currency)); - } - - if (target('addon') && elems.addon) { - addons(); - } - - if (target('coupon') && elems.coupon) { - pricing = pricing.coupon(dom.value(elems.coupon)).then(null, ignoreBadCoupons); - } - - if (target('country') || target('postal_code') || target('vat_number')) { - pricing = pricing.address({ - country: dom.value(elems.country), - postal_code: dom.value(elems.postal_code), - vat_number: dom.value(elems.vat_number) - }); - } - - pricing.done(); - - function addons () { - each(elems.addon, function (node) { - var plan = self.items.plan; - var addonCode = dom.data(node, 'recurlyAddon'); - if (plan.addons && find(plan.addons, { code: addonCode })) { - pricing = pricing.addon(addonCode, { quantity: dom.value(node) }); - } - }); - } - - function target (name) { - if (!targetName) return true; - if (targetName === name) return true; - return false - } - }; - - function update (price) { - dom.value(elems.currency_code, price.currency.code); - dom.value(elems.currency_symbol, price.currency.symbol); - each(['addons', 'discount', 'setup_fee', 'subtotal', 'tax', 'total'], function (value) { - dom.value(elems[value + '_now'], price.now[value]); - dom.value(elems[value + '_next'], price.next[value]); - }); - if (elems.addonPrice) { - each(elems.addonPrice, function (elem) { - var addonPrice = price.addons[dom.data(elem, 'recurlyAddon')]; - if (addonPrice) dom.value(elem, addonPrice); - }); - } - } - - function detatch () { - each(elems, function (name, elems) { - each(elems, function (elem) { - events.unbind(elem, 'change', change); - events.unbind(elem, 'propertychange', change); - }, this); - }, this); - } -}; - -function ignoreBadCoupons (err) { - if (err.code === 'not-found') return; - else throw err; -} - -/** - * Backward-compatibility - * - * @deprecated - */ - -exports.binding = exports.attach; - -}); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -require.alias("visionmedia-node-querystring/index.js", "recurly/deps/querystring/index.js"); -require.alias("visionmedia-node-querystring/index.js", "querystring/index.js"); - -require.alias("component-emitter/index.js", "recurly/deps/emitter/index.js"); -require.alias("component-emitter/index.js", "emitter/index.js"); - -require.alias("component-indexof/index.js", "recurly/deps/indexof/index.js"); -require.alias("component-indexof/index.js", "indexof/index.js"); - -require.alias("component-object/index.js", "recurly/deps/object/index.js"); -require.alias("component-object/index.js", "object/index.js"); - -require.alias("component-event/index.js", "recurly/deps/event/index.js"); -require.alias("component-event/index.js", "event/index.js"); - -require.alias("component-clone/index.js", "recurly/deps/clone/index.js"); -require.alias("component-clone/index.js", "clone/index.js"); -require.alias("component-type/index.js", "component-clone/deps/type/index.js"); - -require.alias("component-bind/index.js", "recurly/deps/bind/index.js"); -require.alias("component-bind/index.js", "bind/index.js"); - -require.alias("component-each/index.js", "recurly/deps/each/index.js"); -require.alias("component-each/index.js", "each/index.js"); -require.alias("component-to-function/index.js", "component-each/deps/to-function/index.js"); -require.alias("component-props/index.js", "component-to-function/deps/props/index.js"); - -require.alias("component-type/index.js", "component-each/deps/type/index.js"); - -require.alias("component-find/index.js", "recurly/deps/find/index.js"); -require.alias("component-find/index.js", "find/index.js"); -require.alias("component-to-function/index.js", "component-find/deps/to-function/index.js"); -require.alias("component-props/index.js", "component-to-function/deps/props/index.js"); - -require.alias("component-json/index.js", "recurly/deps/json/index.js"); -require.alias("component-json/index.js", "json/index.js"); - -require.alias("component-type/index.js", "recurly/deps/type/index.js"); -require.alias("component-type/index.js", "type/index.js"); - -require.alias("component-trim/index.js", "recurly/deps/trim/index.js"); -require.alias("component-trim/index.js", "trim/index.js"); - -require.alias("component-map/index.js", "recurly/deps/map/index.js"); -require.alias("component-map/index.js", "map/index.js"); -require.alias("component-to-function/index.js", "component-map/deps/to-function/index.js"); -require.alias("component-props/index.js", "component-to-function/deps/props/index.js"); - -require.alias("yields-merge/index.js", "recurly/deps/merge/index.js"); -require.alias("yields-merge/index.js", "merge/index.js"); - -require.alias("learnboost-jsonp/index.js", "recurly/deps/jsonp/index.js"); -require.alias("learnboost-jsonp/index.js", "recurly/deps/jsonp/index.js"); -require.alias("learnboost-jsonp/index.js", "jsonp/index.js"); -require.alias("visionmedia-debug/debug.js", "learnboost-jsonp/deps/debug/debug.js"); -require.alias("visionmedia-debug/debug.js", "learnboost-jsonp/deps/debug/index.js"); -require.alias("visionmedia-debug/debug.js", "visionmedia-debug/index.js"); -require.alias("learnboost-jsonp/index.js", "learnboost-jsonp/index.js"); -require.alias("visionmedia-debug/debug.js", "recurly/deps/debug/debug.js"); -require.alias("visionmedia-debug/debug.js", "recurly/deps/debug/index.js"); -require.alias("visionmedia-debug/debug.js", "debug/index.js"); -require.alias("visionmedia-debug/debug.js", "visionmedia-debug/index.js"); -require.alias("chrissrogers-promise/index.js", "recurly/deps/promise/index.js"); -require.alias("chrissrogers-promise/core.js", "recurly/deps/promise/core.js"); -require.alias("chrissrogers-promise/index.js", "promise/index.js"); -require.alias("johntron-asap/asap.js", "chrissrogers-promise/deps/asap/asap.js"); -require.alias("johntron-asap/asap.js", "chrissrogers-promise/deps/asap/index.js"); -require.alias("johntron-asap/asap.js", "johntron-asap/index.js"); -require.alias("kewah-mixin/index.js", "recurly/deps/mixin/index.js"); -require.alias("kewah-mixin/index.js", "recurly/deps/mixin/index.js"); -require.alias("kewah-mixin/index.js", "mixin/index.js"); -require.alias("kewah-mixin/index.js", "kewah-mixin/index.js"); -require.alias("pluma-par/dist/par.js", "recurly/deps/par/dist/par.js"); -require.alias("pluma-par/dist/par.js", "recurly/deps/par/index.js"); -require.alias("pluma-par/dist/par.js", "par/index.js"); -require.alias("pluma-par/dist/par.js", "pluma-par/index.js"); -require.alias("ianstormtaylor-to-slug-case/index.js", "recurly/deps/to-slug-case/index.js"); -require.alias("ianstormtaylor-to-slug-case/index.js", "to-slug-case/index.js"); -require.alias("ianstormtaylor-to-space-case/index.js", "ianstormtaylor-to-slug-case/deps/to-space-case/index.js"); -require.alias("ianstormtaylor-to-no-case/index.js", "ianstormtaylor-to-space-case/deps/to-no-case/index.js"); - -require.alias("recurly/lib/index.js", "recurly/index.js");if (typeof exports == "object") { - module.exports = require("recurly"); -} else if (typeof define == "function" && define.amd) { - define([], function(){ return require("recurly"); }); -} else { - this["recurly"] = require("recurly"); -}})(); \ No newline at end of file diff --git a/services/web/public/js/libs/recurly-4.8.5.js b/services/web/public/js/libs/recurly-4.8.5.js new file mode 100644 index 0000000000..f638c14904 --- /dev/null +++ b/services/web/public/js/libs/recurly-4.8.5.js @@ -0,0 +1,6 @@ +var recurly=function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/build/",t(t.s=36)}([function(e,t,n){(function(r){function i(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function o(e){var n=this.useColors;if(e[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+e[0]+(n?"%c ":" ")+"+"+t.humanize(this.diff),n){var r="color: "+this.color;e.splice(1,0,r,"color: inherit");var i=0,o=0;e[0].replace(/%[a-zA-Z%]/g,function(e){"%%"!==e&&(i++,"%c"===e&&(o=i))}),e.splice(o,0,r)}}function a(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function u(e){try{null==e?t.storage.removeItem("debug"):t.storage.debug=e}catch(e){}}function s(){var e;try{e=t.storage.debug}catch(e){}return!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG),e}t=e.exports=n(40),t.log=a,t.formatArgs=o,t.save=u,t.load=s,t.useColors=i,t.storage="undefined"!=typeof chrome&&void 0!==chrome.storage?chrome.storage.local:function(){try{return window.localStorage}catch(e){}}(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.formatters.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},t.enable(s())}).call(t,n(39))},function(e,t,n){"use strict";function r(e,t){return r.get(e,t)}var i=n(23);e.exports=r,r.map={},r.baseURL="",r.doc=function(e){r.baseURL=e},r.get=function(e,t){if(e in r.map)return new r.map[e](t);throw new Error("invalid error: "+e)},r.add=function(e,t){function n(n){Error.call(this),this.name=this.code=e,t.message instanceof Function?this.message=t.message(n):this.message=t.message,i(this,n||{}),t.help&&(this.help=r.baseURL+t.help,this.message+=" (need help? "+this.help+")")}return t=t||{},n.prototype=new Error,r.map[e]=n},r.doc("https://docs.recurly.com/js"),r.add("already-configured",{message:"Configuration may only be set once.",help:"#identify-your-site"}),r.add("config-missing-public-key",{message:"The publicKey setting is required.",help:"#identify-your-site"}),r.add("config-missing-fields",{message:"The fields setting is required."}),r.add("missing-hosted-field-target",{message:function(e){return"Target element not found for "+e.type+" field using selector '"+e.selector+"'"}}),r.add("api-error",{message:"There was an error with your request."}),r.add("api-timeout",{message:"The API request timed out."}),r.add("validation",{message:"There was an error validating your request."}),r.add("missing-callback",{message:"Missing callback"}),r.add("invalid-options",{message:"Options must be an object"}),r.add("invalid-option",{message:function(e){return"Option "+e.name+" must be "+e.expect}}),r.add("missing-plan",{message:"A plan must be specified."}),r.add("missing-coupon",{message:"A coupon must be specified."}),r.add("invalid-item",{message:"The given item does not appear to be a valid recurly plan, coupon, addon, or taxable address."}),r.add("invalid-addon",{message:"The given addon_code is not among the valid addons for the specified plan."}),r.add("invalid-currency",{message:function(e){return"The given currency ("+e.currency+") is not among the valid codes for the specified plan(s): "+e.allowed+"."}}),r.add("invalid-plan-currency",{message:function(e){return"The requested plan ("+e.planCode+") does not support the possible checkout currencies: "+e.currencies+"."}}),r.add("invalid-subscription-currency",{message:"The given subscription does not support the currencies of this Checkout instance's existing subscriptions"}),r.add("unremovable-item",{message:"The given item cannot be removed."}),r.add("fraud-data-collector-request-failed",{message:function(e){return"There was an error getting the data collector fields: "+e.error}}),r.add("fraud-data-collector-missing-form",{message:function(e){return"There was an error finding a form to inject the data collector fields using selector '"+e.selector+"'"}}),r.add("gift-card-currency-mismatch",{message:"The giftcard currency does not match the given currency."}),r.add("apple-pay-not-supported",{message:"Apple Pay is not supported by this device or browser."}),r.add("apple-pay-not-available",{message:"Apple Pay is supported by this device, but the customer has not configured Apple Pay."}),r.add("apple-pay-config-missing",{message:function(e){return"Missing Apple Pay configuration option: '"+e.opt+"'"}}),r.add("apple-pay-config-invalid",{message:function(e){return"Apple Pay configuration option '"+e.opt+"' is not among your available options: "+e.set+".\n Please refer to your site configuration if the available options is incorrect."}}),r.add("apple-pay-factory-only",{message:"Apple Pay must be initialized by calling recurly.ApplePay"}),r.add("apple-pay-init-error",{message:function(e){var t="Apple Pay did not initialize due to a fatal error";return e.err&&(t+=": "+e.err.message),t}}),r.add("apple-pay-payment-failure",{message:"Apply Pay could not charge the customer"}),r.add("paypal-factory-only",{message:"PayPal must be initialized by calling recurly.PayPal"}),r.add("paypal-config-missing",{message:function(e){return"Missing PayPal configuration option: '"+e.opt+"'"}}),r.add("paypal-load-error",{message:"Client libraries failed to load"}),r.add("paypal-client-error",{message:"PayPal encountered an unexpected error"}),r.add("paypal-tokenize-error",{message:"An error occurred while attempting to generate the PayPal token"}),r.add("paypal-tokenize-recurly-error",{message:"An error occurred while attempting to generate the Recurly token"}),r.add("paypal-braintree-not-ready",{message:"Braintree PayPal is not yet ready to create a checkout flow"}),r.add("paypal-braintree-api-error",{message:"Braintree API experienced an error"}),r.add("paypal-braintree-tokenize-braintree-error",{message:"An error occurred while attempting to generate the Braintree token"}),r.add("paypal-braintree-tokenize-recurly-error",{message:"An error occurred while attempting to generate the Braintree token within Recurly"})},function(e,t){function n(e){if(e)return r(e)}function r(e){for(var t in n.prototype)e[t]=n.prototype[t];return e}e.exports=n,n.prototype.on=n.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks[e]=this._callbacks[e]||[]).push(t),this},n.prototype.once=function(e,t){function n(){r.off(e,n),t.apply(this,arguments)}var r=this;return this._callbacks=this._callbacks||{},n.fn=t,this.on(e,n),this},n.prototype.off=n.prototype.removeListener=n.prototype.removeAllListeners=n.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks[e];if(!n)return this;if(1==arguments.length)return delete this._callbacks[e],this;for(var r,i=0;i0&&void 0!==arguments[0]?arguments[0]:12;return Math.random().toString(36).substr(2,e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r},function(e,t,n){"use strict";e.exports=n(44)},function(e,t,n){"use strict";function r(){}function i(e){try{return e.then}catch(e){return m=e,b}}function o(e,t){try{return e(t)}catch(e){return m=e,b}}function a(e,t,n){try{e(t,n)}catch(e){return m=e,b}}function u(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._45=0,this._81=0,this._65=null,this._54=null,e!==r&&y(e,this)}function s(e,t,n){return new e.constructor(function(i,o){var a=new u(r);a.then(i,o),c(e,new h(t,n,a))})}function c(e,t){for(;3===e._81;)e=e._65;if(u._10&&u._10(e),0===e._81)return 0===e._45?(e._45=1,void(e._54=t)):1===e._45?(e._45=2,void(e._54=[e._54,t])):void e._54.push(t);l(e,t)}function l(e,t){v(function(){var n=1===e._81?t.onFulfilled:t.onRejected;if(null===n)return void(1===e._81?f(t.promise,e._65):d(t.promise,e._65));var r=o(n,e._65);r===b?d(t.promise,m):f(t.promise,r)})}function f(e,t){if(t===e)return d(e,new TypeError("A promise cannot be resolved with itself."));if(t&&("object"==typeof t||"function"==typeof t)){var n=i(t);if(n===b)return d(e,m);if(n===e.then&&t instanceof u)return e._81=3,e._65=t,void p(e);if("function"==typeof n)return void y(n.bind(t),e)}e._81=1,e._65=t,p(e)}function d(e,t){e._81=2,e._65=t,u._97&&u._97(e,t),p(e)}function p(e){if(1===e._45&&(c(e,e._54),e._54=null),2===e._45){for(var t=0;t0&&void 0!==arguments[0]?arguments[0]:{},n="input";~["button","select"].indexOf(e.type)&&(n=e.type,delete e.type);var r=t.document.createElement(n);return"type"in e||(e.type="text"),"style"in e||(e.style="position: absolute; top: 0px; left: -1000px; opacity: 0;"),Object.keys(e).forEach(function(t){return r.setAttribute(t,e[t])}),r}var d=n(54),p=n(4);e.exports={createHiddenInput:f,data:u,element:r,findNodeInParents:l,value:i}}).call(t,n(3))},function(e,t,n){function r(e){return function(t){for(var n in e)if(t[n]!=e[n])return!1;return!0}}var i=n(64);e.exports=function(e,t){"function"!=typeof t&&(t=Object(t)===t?r(t):i(t));for(var n=0,o=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},r=n.internal,i=void 0!==r&&r;return this.debug("reprice"),new v.default(function(e,n){new t.Calculations(t,function(n){if(JSON.stringify(n)===JSON.stringify(t.price))return e(n);t.price=n,i||t.emit("change:external",n),t.emit("change",n),e(n)})},this).nodeify(e)}},{key:"remove",value:function(e,t){var n=this,r=void 0;return this.debug("remove"),new v.default(function(t,i){var o=Object.keys(e)[0],a=e[o];if(!~Object.keys(n.items).indexOf(o))return n.error((0,b.default)("invalid-item"),i);if(Array.isArray(n.items[o])){var s=n.items[o].indexOf(u(n.items[o],{code:a}));~s&&(r=n.items[o].splice(s))}else{if(!n.items[o]||a!==n.items[o].code&&!0!==a)return n.error((0,b.default)("unremovable-item",{type:o,id:a,reason:"does not exist on this pricing instance."}),i);r=n.items[o],delete n.items[o]}t()},this).nodeify(t)}},{key:"itemUpdateFactory",value:function(e,t){var n=this;return function(r,i){if(JSON.stringify(t)===JSON.stringify(n.items[e]))return r(n.items[e]);n.items[e]=t,_("set."+e),n.emit("set."+e,t),r(t)}}},{key:"error",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){},r=arguments[2];return r&&r.split(".").reduce(function(n,r){return t.emit(n+"."+r,e)},"error"),this.emit("error",e),n(e)}},{key:"Calculations",get:function(){throw new Error("Not implemented")}},{key:"PRICING_METHODS",get:function(){return["reset","remove","reprice"]}},{key:"hasPrice",get:function(){return!!this.price}},{key:"totalNow",get:function(){return(0,w.default)(this.hasPrice?this.price.now.total:0)}},{key:"subtotalPreDiscountNow",get:function(){var e=parseFloat(this.price.now.subtotal)+parseFloat(this.price.now.discount);return(0,w.default)(this.hasPrice?e:0)}},{key:"currencyCode",get:function(){return this.items.currency||""}},{key:"currencySymbol",get:function(){return(0,l.default)(this.currencyCode)}}]),t}(d.default);t.Pricing=O},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n1?r-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:{},r=n.id,a=void 0===r?(0,w.default)():r;i(this,t);var u=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return u.id=a,u.debug=_,u}return a(t,e),s(t,[{key:"reset",value:function(){c(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"reset",this).call(this),this.items.addons=[]}},{key:"attach",value:function(e){var t=this;return this.attachment&&this.attachment.detach(),this.attachment=new v.default(this,e),this.attachment.once("ready",function(){return t.emit("attached")}),this.attachment}},{key:"plan",value:function(){var e=this,t=this.resolvePlanOptions.apply(this,arguments),n=t.currentPlan,r=t.quantity,i=t.planCode,o=(t.options,t.done);return new d.default(function(t,o){if(n&&n.code===i)return n.quantity=r,t(n);e.recurly.plan(i,function(n,i){if(n)return e.error(n,o,"plan");var a=function(){_("set.plan"),e.emit("set.plan",i),t(i)};i.quantity=r,e.items.plan=i,e.items.currency in i.price||e.currency(Object.keys(i.price)[0]),e.items.coupon?e.coupon(e.items.coupon.code).then(a,a):a()})},this).nodeify(o)}},{key:"addon",value:function(e,t,n){var r=this;return"function"==typeof t&&(n=t,t=void 0),t=t||{},new d.default(function(n,i){if(!r.items.plan)return r.error((0,b.default)("missing-plan"),i,"addon");var o=(0,l.findByCode)(r.items.plan.addons,e);if(!o)return r.error((0,b.default)("invalid-addon",{planCode:r.items.plan.code,addonCode:e}),i,"addon");var a=u(t,o),s=(0,l.findByCode)(r.items.addons,e);0===a&&r.remove({addon:e}),s?s.quantity=a:(s=JSON.parse(JSON.stringify(o)),s.quantity=a,r.items.addons.push(s)),_("set.addon"),r.emit("set.addon",s),n(s)},this).nodeify(n)}},{key:"giftcard",value:function(e,t){function n(e){_("set.gift_card"),i.items.gift_card=e,i.emit("set.gift_card",e)}function r(e){_("unset.gift_card"),delete i.items.gift_card,i.emit("unset.gift_card")}var i=this;return new d.default(function(t,o){if(r(),!e)return t();i.recurly.giftcard({giftcard:e},function(e,a){return e&&"not-found"===e.code&&r(),e?i.error(e,o,"gift_card"):i.items.currency!==a.currency?(r(),i.error((0,b.default)("gift-card-currency-mismatch"),o,"gift_card")):(n(a),void t(a))})},this).nodeify(t)}},{key:"coupon",value:function(e,t){var n=this,r=function(){_("unset.coupon"),delete n.items.coupon,n.emit("unset.coupon")};return new d.default(function(t,i){if(!n.items.plan)return n.error((0,b.default)("missing-plan"),i,"coupon");if(n.items.coupon&&n.remove({coupon:n.items.coupon.code}),!e)return r(),t();var o=function(e,o){return e&&"not-found"===e.code&&r(),e?n.error(e,i,"coupon"):n.couponIsValidForSubscription(o)?(_("set.coupon"),n.items.coupon=o,n.emit("set.coupon",o),t(o),void 0):n.error("invalid-coupon-for-subscription",i,"coupon")};"string"==typeof e?n.recurly.coupon({plan:n.items.plan.code,coupon:e},o):o(null,e)},this).nodeify(t)}},{key:"address",value:function(e,t){return new d.default(this.itemUpdateFactory("address",e),this).nodeify(t)}},{key:"shippingAddress",value:function(e,t){return new d.default(this.itemUpdateFactory("shipping_address",e),this).nodeify(t)}},{key:"tax",value:function(e,t){return new d.default(this.itemUpdateFactory("tax",e),this).nodeify(t)}},{key:"currency",value:function(e,t){var n=this,r=this.items.plan,i=this.items.currency;return new d.default(function(t,o){return i===e?t(i):!r||e in r.price?(n.items.currency=e,_("set.currency"),n.emit("set.currency",e),void t(e)):n.error((0,b.default)("invalid-currency",{currency:e,allowed:Object.keys(r.price)}),o,"currency")},this).nodeify(t)}},{key:"couponIsValidForSubscription",value:function(e){return!!e&&(!!e.applies_to_plans&&(!!e.applies_to_all_plans||!!~e.plans.indexOf(this.items.plan.code)))}},{key:"resolvePlanOptions",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments[2],r=this.items.plan,i=void 0;return"function"==typeof t&&(n=t,t={}),r&&r.quantity&&(i=r.quantity),t.quantity&&(i=parseInt(t.quantity,10)),(!i||i<1)&&(i=1),{currentPlan:r,quantity:i,planCode:e,options:t,done:n}}},{key:"Calculations",get:function(){return h.default}},{key:"PRICING_METHODS",get:function(){return c(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"PRICING_METHODS",this).concat(["addon","address","coupon","currency","giftcard","plan","shippingAddress","tax"])}},{key:"isValid",get:function(){return!(!this.items.plan||!this.price)}},{key:"taxCode",get:function(){if(this.items.tax)return this.items.tax.taxCode||this.items.tax.tax_code}},{key:"taxExempt",get:function(){return this.items.plan&&this.items.plan.tax_exempt}}]),t}(l.Pricing);t.default=O},function(e,t,n){function r(e){switch(i(e)){case"object":var t={};for(var n in e)e.hasOwnProperty(n)&&(t[n]=r(e[n]));return t;case"array":for(var t=new Array(e.length),o=0,a=e.length;o-1&&e%1==0&&e<=w}function m(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function b(e){return!!e&&"object"==typeof e}function g(){return!1}var w=9007199254740991,_="[object Arguments]",O="[object Function]",j="[object GeneratorFunction]",x="[object Map]",k="[object Set]",C=/[\\^$.*+?()[\]{}|]/g,P=/^\[object .+?Constructor\]$/,S="object"==typeof e&&e&&e.Object===Object&&e,E="object"==typeof self&&self&&self.Object===Object&&self,F=S||E||Function("return this")(),A="object"==typeof t&&t&&!t.nodeType&&t,T=A&&"object"==typeof n&&n&&!n.nodeType&&n,M=T&&T.exports===A,I=Function.prototype,N=Object.prototype,R=F["__core-js_shared__"],L=function(){var e=/[^.]+$/.exec(R&&R.keys&&R.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),D=I.toString,q=N.hasOwnProperty,B=N.toString,z=RegExp("^"+D.call(q).replace(C,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),U=M?F.Buffer:void 0,V=N.propertyIsEnumerable,$=U?U.isBuffer:void 0,H=function(e,t){return function(n){return e(t(n))}}(Object.keys,Object),K=u(F,"DataView"),G=u(F,"Map"),W=u(F,"Promise"),J=u(F,"Set"),Z=u(F,"WeakMap"),Y=!V.call({valueOf:1},"valueOf"),X=l(K),Q=l(G),ee=l(W),te=l(J),ne=l(Z),re=o;(K&&"[object DataView]"!=re(new K(new ArrayBuffer(1)))||G&&re(new G)!=x||W&&"[object Promise]"!=re(W.resolve())||J&&re(new J)!=k||Z&&"[object WeakMap]"!=re(new Z))&&(re=function(e){var t=B.call(e),n="[object Object]"==t?e.constructor:void 0,r=n?l(n):void 0;if(r)switch(r){case X:return"[object DataView]";case Q:return x;case ee:return"[object Promise]";case te:return k;case ne:return"[object WeakMap]"}return t});var ie=Array.isArray,oe=$||g;n.exports=h}).call(t,n(3),n(81)(e))},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?e():this.once("ready",e)}},{key:"configure",value:function(e){if(te("configure"),e=(0,h.default)(e),"string"==typeof e&&(e={publicKey:e}),e=c(e),e.publicKey)this.config.publicKey=e.publicKey;else if(!this.config.publicKey)throw(0,C.default)("config-missing-public-key");e.api&&(this.config.api=e.api),e.currency&&(this.config.currency=e.currency),"cors"in e&&(this.config.cors=e.cors),"fraud"in e&&(0,Y.default)(this.config.fraud,e.fraud),"parent"in e&&(this.config.parent=e.parent),"object"===f(e.fields)&&(0,Y.default)(this.config.fields,e.fields),this.config.required=e.required||this.config.required||[],this.config.parent?this.parent():e.parentVersion&&(this.config.parentVersion=e.parentVersion),this.configured=!0}},{key:"parent",value:function(){var e=this,t=!1;if(this.hostedFields&&this.readyState>1&&!this.hostedFields.integrityCheck(this.config.fields)&&(t=!0),t&&(this.readyState=0,this.hostedFields.reset()),this.readyState>0)return void this.bus.send("hostedFields:configure",{recurly:this.config});this.fraud||(this.fraud=new $.Fraud(this)),this.bus&&this.bus.stop(),this.bus=new V.Bus({api:this.config.api}),this.bus.add(this),this.hostedFields&&!t||(this.hostedFields=new H.HostedFields({recurly:this.config})),0===this.hostedFields.errors.length?(this.bus.add(this.hostedFields),this.once("hostedFields:ready",function(t){e.readyState=2,e.emit("ready")}),this.on("hostedFields:state:change",function(t){return e.emit("change",{fields:t})}),this.on("hostedField:submit",function(){return e.emit("field:submit")}),this.readyState=1):(this.readyState=3,this.emit("ready"))}},{key:"url",value:function(e){return this.config.api+e}},{key:"request",value:function(e,t,n,r){if(te("request",e,t,n),!this.configured)throw(0,C.default)("not-configured");return"function"==typeof n&&(r=n,n={}),this.config.parent?n.version=this.version:n.version=this.config.parentVersion,n.key=this.config.publicKey,this.config.cors?this.xhr(e,t,n,r):this.jsonp(t,n,r)}},{key:"cachedRequest",value:function(e,t,n,r){var i=this._cachedRequests=this._cachedRequests||{},a=[e,t,JSON.stringify(n)].join("-");i[a]?r.apply(void 0,[null].concat(o(i[a]))):this.request(e,t,n,function(e){for(var t=arguments.length,n=Array(t>1?t-1:0),o=1;o0?e(f.reduce(function(e,t){return e.concat(t)})):n(d[0]))};p.forEach(function(e){return t.request(r,o,e,i)})})}},{key:"xhr",value:function(e,t,n,r){function i(){"post"===e?(a.setRequestHeader&&a.setRequestHeader("Content-type","application/x-www-form-urlencoded"),a.send(s)):a.send()}te("xhr");var o=function(){var e=window.XMLHttpRequest,t=window.XDomainRequest;return e&&"withCredentials"in new e?e:t||void 0}(),a=new o,u=this.url(t),s=w.default.stringify(n,{encodeValuesOnly:!0});"get"===e&&(u+="?"+s),a.open(e,u),a.timeout=this.config.timeout,a.ontimeout=function(){r((0,C.default)("api-timeout"))},a.onerror=function(){r((0,C.default)("api-error"))},a.onprogress=function(){},a.onload=function(){var e;try{e=JSON.parse(this.responseText)}catch(e){return te(this.responseText,e),r((0,C.default)("api-error",{message:"There was a problem parsing the API response with: "+this.responseText}))}e&&e.error?r((0,C.default)("api-error",e.error)):r(null,e)},setTimeout(i,0)}},{key:"jsonp",value:function(e,t,n){te("jsonp");var r=this.url(e)+"?"+w.default.stringify(t,{encodeValuesOnly:!0});(0,b.default)(r,{timeout:this.config.timeout,prefix:"__rjs"},function(e,t){if(e)return n(e);t.error?n((0,C.default)("api-error",t.error)):n(null,t)})}}]),t}(O.default);re.prototype.Frame=D.factory,re.prototype.coupon=T.default,re.prototype.giftCard=I.default,re.prototype.giftcard=I.default,re.prototype.ApplePay=q.factory,re.prototype.PayPal=B.factory,re.prototype.paypal=U.deprecated,re.prototype.Adyen=z.factory,re.prototype.plan=n(89),re.prototype.tax=R.default,re.prototype.token=L.token,re.prototype.validate=n(90)},function(e,t,n){"use strict";var r=n(42),i=n(43),o=n(21);e.exports={formats:o,parse:i,stringify:r}},function(e,t,n){"use strict";var r=Object.prototype.hasOwnProperty,i=function(){for(var e=[],t=0;t<256;++t)e.push("%"+((t<16?"0":"")+t.toString(16)).toUpperCase());return e}(),o=function(e){for(var t;e.length;){var n=e.pop();if(t=n.obj[n.prop],Array.isArray(t)){for(var r=[],i=0;i=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122?n+=t.charAt(r):o<128?n+=i[o]:o<2048?n+=i[192|o>>6]+i[128|63&o]:o<55296||o>=57344?n+=i[224|o>>12]+i[128|o>>6&63]+i[128|63&o]:(r+=1,o=65536+((1023&o)<<10|1023&t.charCodeAt(r)),n+=i[240|o>>18]+i[128|o>>12&63]+i[128|o>>6&63]+i[128|63&o])}return n},t.compact=function(e){for(var t=[{obj:{o:e},prop:"o"}],n=[],r=0;rc){for(var t=0,n=a.length-s;t0)throw this.hostedFields.errors[0];var a=(0,l.default)();this.bus.send("token:init",{id:a,inputs:i}),this.once("token:done:"+a,function(e){return n(e.err,e.token)})}else{var u=o.call(this,i);if(u.length)return t((0,d.default)("validation",{fields:u}));this.request("post","/token",i,n)}}function o(e){var t=[];return this.validate.cardNumber(e.number)||t.push("number"),this.validate.expiry(e.month,e.year)||t.push("month","year"),e.first_name||t.push("first_name"),e.last_name||t.push("last_name"),!~this.config.required.indexOf("cvv")&&!e.cvv||this.validate.cvv(e.cvv)||t.push("cvv"),(0,u.default)(this.config.required,function(n){!e[n]&&~h.indexOf(n)&&t.push(n)}),p("validate errors",t),t}Object.defineProperty(t,"__esModule",{value:!0}),t.FIELDS=void 0,t.token=i;var a=n(4),u=r(a),s=n(15),c=n(5),l=r(c),f=n(1),d=r(f),p=n(0)("recurly:token"),h=t.FIELDS=["first_name","last_name","address1","address2","company","country","city","state","postal_code","phone","vat_number","fraud_session_id","token"]},function(e,t){function n(e){return e.replace(/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^\/]+)\//g,"").replace(a,"").match(/[a-zA-Z_]\w*/g)||[]}function r(e,t,n){var r=/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^\/]+)\/|[a-zA-Z_]\w*/g;return e.replace(r,function(e){return"("==e[e.length-1]?n(e):~t.indexOf(e)?n(e):e})}function i(e){for(var t=[],n=0;n1?t-1:0),a=1;a1&&void 0!==arguments[1]?arguments[1]:function(e){return e};return e.reduce(function(e,n){var r=t(n);return(e[r]=e[r]||[]).push(n),e},{})}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r},function(e,t,n){"use strict";var r=n(18);e.exports=t=new r.Recurly,t.Recurly=r.Recurly},function(e,t){function n(e,t,n){var r=-1,i=e.length;t<0&&(t=-t>i?0:i+t),n=n>i?i:n,n<0&&(n+=i),i=t>n?0:n-t>>>0,t>>>=0;for(var o=Array(i);++r-1&&e%1==0&&e-1&&e%1==0&&e<=m}function l(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function f(e){return!!e&&"object"==typeof e}function d(e){return"symbol"==typeof e||f(e)&&F.call(e)==O}function p(e){if(!e)return 0===e?e:0;if((e=y(e))===v||e===-v){return(e<0?-1:1)*b}return e===e?e:0}function h(e){var t=p(e),n=t%1;return t===t?n?t-n:t:0}function y(e){if("number"==typeof e)return e;if(d(e))return g;if(l(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=l(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(j,"");var n=k.test(e);return n||C.test(e)?S(e.slice(2),n?2:8):x.test(e)?g:+e}var v=1/0,m=9007199254740991,b=1.7976931348623157e308,g=NaN,w="[object Function]",_="[object GeneratorFunction]",O="[object Symbol]",j=/^\s+|\s+$/g,x=/^[-+]0x[0-9a-f]+$/i,k=/^0b[01]+$/i,C=/^0o[0-7]+$/i,P=/^(?:0|[1-9]\d*)$/,S=parseInt,E=Object.prototype,F=E.toString,A=Math.ceil,T=Math.max;e.exports=o},function(e,t,n){function r(){}function i(e,t,n){function i(){u.parentNode.removeChild(u),window[h]=r}"function"==typeof t&&(n=t,t={}),t||(t={});var u,s,c=t.prefix||"__jp",l=t.param||"callback",f=null!=t.timeout?t.timeout:6e4,d=encodeURIComponent,p=document.getElementsByTagName("script")[0]||document.head,h=c+a++;f&&(s=setTimeout(function(){i(),n&&n(new Error("Timeout"))},f)),window[h]=function(e){o("jsonp got",e),s&&clearTimeout(s),i(),n&&n(null,e)},e+=(~e.indexOf("?")?"&":"?")+l+"="+d(h),e=e.replace("?&","?"),o('jsonp req "%s"',e),u=document.createElement("script"),u.src=e,p.parentNode.insertBefore(u,p)}var o=n(0)("jsonp");e.exports=i;var a=0},function(e,t){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function i(e){if(l===setTimeout)return setTimeout(e,0);if((l===n||!l)&&setTimeout)return l=setTimeout,setTimeout(e,0);try{return l(e,0)}catch(t){try{return l.call(null,e,0)}catch(t){return l.call(this,e,0)}}}function o(e){if(f===clearTimeout)return clearTimeout(e);if((f===r||!f)&&clearTimeout)return f=clearTimeout,clearTimeout(e);try{return f(e)}catch(t){try{return f.call(null,e)}catch(t){return f.call(this,e)}}}function a(){y&&p&&(y=!1,p.length?h=p.concat(h):v=-1,h.length&&u())}function u(){if(!y){var e=i(a);y=!0;for(var t=h.length;t;){for(p=h,h=[];++v1)for(var n=1;n100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var n=parseFloat(t[1]);switch((t[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return n*l;case"days":case"day":case"d":return n*c;case"hours":case"hour":case"hrs":case"hr":case"h":return n*s;case"minutes":case"minute":case"mins":case"min":case"m":return n*u;case"seconds":case"second":case"secs":case"sec":case"s":return n*a;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(e){return e>=c?Math.round(e/c)+"d":e>=s?Math.round(e/s)+"h":e>=u?Math.round(e/u)+"m":e>=a?Math.round(e/a)+"s":e+"ms"}function i(e){return o(e,c,"day")||o(e,s,"hour")||o(e,u,"minute")||o(e,a,"second")||e+" ms"}function o(e,t,n){if(!(e0)return n(e);if("number"===o&&!1===isNaN(e))return t.long?i(e):r(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},function(e,t,n){"use strict";var r=n(20),i=n(21),o={brackets:function(e){return e+"[]"},indices:function(e,t){return e+"["+t+"]"},repeat:function(e){return e}},a=Date.prototype.toISOString,u={delimiter:"&",encode:!0,encoder:r.encode,encodeValuesOnly:!1,serializeDate:function(e){return a.call(e)},skipNulls:!1,strictNullHandling:!1},s=function e(t,n,i,o,a,s,c,l,f,d,p,h){var y=t;if("function"==typeof c)y=c(n,y);else if(y instanceof Date)y=d(y);else if(null===y){if(o)return s&&!h?s(n,u.encoder):n;y=""}if("string"==typeof y||"number"==typeof y||"boolean"==typeof y||r.isBuffer(y)){if(s){return[p(h?n:s(n,u.encoder))+"="+p(s(y,u.encoder))]}return[p(n)+"="+p(String(y))]}var v=[];if(void 0===y)return v;var m;if(Array.isArray(c))m=c;else{var b=Object.keys(y);m=l?b.sort(l):b}for(var g=0;g0?P+C:""}},function(e,t,n){"use strict";var r=n(20),i=Object.prototype.hasOwnProperty,o={allowDots:!1,allowPrototypes:!1,arrayLimit:20,decoder:r.decode,delimiter:"&",depth:5,parameterLimit:1e3,plainObjects:!1,strictNullHandling:!1},a=function(e,t){for(var n={},r=t.ignoreQueryPrefix?e.replace(/^\?/,""):e,a=t.parameterLimit===1/0?void 0:t.parameterLimit,u=r.split(t.delimiter,a),s=0;s=0;--i){var o,a=e[i];if("[]"===a)o=[],o=o.concat(r);else{o=n.plainObjects?Object.create(null):{};var u="["===a.charAt(0)&&"]"===a.charAt(a.length-1)?a.slice(1,-1):a,s=parseInt(u,10);!isNaN(s)&&a!==u&&String(s)===u&&s>=0&&n.parseArrays&&s<=n.arrayLimit?(o=[],o[s]=r):o[u]=r}r=o}return r},s=function(e,t,n){if(e){var r=n.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e,o=/(\[[^[\]]*])/,a=/(\[[^[\]]*])/g,s=o.exec(r),c=s?r.slice(0,s.index):r,l=[];if(c){if(!n.plainObjects&&i.call(Object.prototype,c)&&!n.allowPrototypes)return;l.push(c)}for(var f=0;null!==(s=a.exec(r))&&f "+t+") {","args = new Array(arguments.length + 1);","for (var i = 0; i < arguments.length; i++) {","args[i] = arguments[i];","}","}","return new Promise(function (rs, rj) {","var cb = "+u+";","var res;","switch (argLength) {",n.concat(["extra"]).map(function(e,t){return"case "+t+":res = fn.call("+["self"].concat(n.slice(0,t)).concat("cb").join(",")+");break;"}).join(""),"default:","args[argLength] = cb;","res = fn.apply(self, args);","}","if (res &&",'(typeof res === "object" || typeof res === "function") &&','typeof res.then === "function"',") {rs(res);}","});","};"].join("");return Function(["Promise","fn"],i)(o,e)}var o=n(7),a=n(49);e.exports=o,o.denodeify=function(e,t){return"number"==typeof t&&t!==1/0?r(e,t):i(e)};var u="function (err, res) {if (err) { rj(err); } else { rs(res); }}";o.nodeify=function(e){return function(){var t=Array.prototype.slice.call(arguments),n="function"==typeof t[t.length-1]?t.pop():null,r=this;try{return e.apply(this,arguments).nodeify(n,r)}catch(e){if(null===n||void 0===n)return new o(function(t,n){n(e)});a(function(){n.call(r,e)})}}},o.prototype.nodeify=function(e,t){if("function"!=typeof e)return this;this.then(function(n){a(function(){e.call(t,null,n)})},function(n){a(function(){e.call(t,n)})})}},function(e,t,n){"use strict";function r(){if(s.length)throw s.shift()}function i(e){var t;t=u.length?u.pop():new o,t.task=e,a(t)}function o(){this.task=null}var a=n(22),u=[],s=[],c=a.makeRequestCallFromTimer(r);e.exports=i,o.prototype.call=function(){try{this.task.call()}catch(e){i.onerror?i.onerror(e):(s.push(e),c())}finally{this.task=null,u[u.length]=this}}},function(e,t,n){"use strict";var r=n(7);e.exports=r,r.enableSynchronous=function(){r.prototype.isPending=function(){return 0==this.getState()},r.prototype.isFulfilled=function(){return 1==this.getState()},r.prototype.isRejected=function(){return 2==this.getState()},r.prototype.getValue=function(){if(3===this._81)return this._65.getValue();if(!this.isFulfilled())throw new Error("Cannot get a value of an unfulfilled promise.");return this._65},r.prototype.getReason=function(){if(3===this._81)return this._65.getReason();if(!this.isRejected())throw new Error("Cannot get a rejection reason of a non-rejected promise.");return this._65},r.prototype.getState=function(){return 3===this._81?this._65.getState():-1===this._81||-2===this._81?0:this._81}},r.disableSynchronous=function(){r.prototype.isPending=void 0,r.prototype.isFulfilled=void 0,r.prototype.isRejected=void 0,r.prototype.getValue=void 0,r.prototype.getReason=void 0,r.prototype.getState=void 0}},function(e,t,n){"use strict";e.exports="4.8.5"},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){d("token");var n=(0,c.normalize)(p,e),r=n.values,i=a(r);if("function"!=typeof t)throw(0,f.default)("missing-callback");if(i.length)return t((0,f.default)("validation",{fields:i}));this.request("post","/token",r,function(e,r){if(e)return t(e);n.fields.token&&r.id&&(n.fields.token.value=r.id),t(null,r)})}function o(e,t){if(d("bankInfo"),"function"!=typeof t)throw(0,f.default)("missing-callback");var n=e&&e.routingNumber;if(!n||"string"!=typeof n)return t((0,f.default)("validation",{fields:["routingNumber"]}));this.request("get","/bank",{routing_number:n},function(e,n){if(e)return t(e);t(null,n)})}function a(e){var t=[];return(0,s.default)(h,function(n){e[n]&&"string"==typeof e[n]||t.push(n)}),e.account_number!==e.account_number_confirmation&&t.push("account_number_confirmation"),d("validate errors",t),t}var u=n(4),s=r(u),c=n(15),l=n(1),f=r(l),d=n(0)("recurly:bankAccount");e.exports={token:i,bankInfo:o};var p=["account_number","account_number_confirmation","routing_number","name_on_account","account_type","address1","address2","company","country","city","state","postal_code","phone","vat_number","token"],h=["account_number","account_number_confirmation","routing_number","account_type","name_on_account","country"]},function(e,t,n){function r(e){switch({}.toString.call(e)){case"[object Object]":return u(e);case"[object Function]":return e;case"[object String]":return a(e);case"[object RegExp]":return o(e);default:return i(e)}}function i(e){return function(t){return e===t}}function o(e){return function(t){return e.test(t)}}function a(e){return/^ *\W+/.test(e)?new Function("_","return _ "+e):new Function("_","return "+s(e))}function u(e){var t={};for(var n in e)t[n]="string"==typeof e[n]?i(e[n]):r(e[n]);return function(e){if("object"!=typeof e)return!1;for(var n in t){if(!(n in e))return!1;if(!t[n](e[n]))return!1}return!0}}function s(e){var t=l(e);if(!t.length)return"_."+e;var n,r,i;for(r=0;r0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];a("%j",e);var n=e.coupon,r=e.plans,i=e.plan,u=e.currency;if(!n)throw(0,o.default)("missing-coupon");if("function"!=typeof t)throw(0,o.default)("missing-callback");!r&&i&&(r=[i]),this.pipedRequest({route:"/coupons/"+n,data:{plan_codes:r,currency:u},by:"plan_codes"}).nodeify(t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(1),o=function(e){return e&&e.__esModule?e:{default:e}}(i),a=n(0)("recurly:coupon")},function(e,t,n){"use strict";function r(e,t){if(u("%j",e),"function"!=typeof t)throw(0,a.default)("missing-callback");if("object"!==(void 0===e?"undefined":i(e)))throw(0,a.default)("invalid-options");if(!("giftcard"in e))throw(0,a.default)("missing-giftcard");this.request("get","/gift_cards/"+e.giftcard,e,t)}Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.default=r;var o=n(1),a=function(e){return e&&e.__esModule?e:{default:e}}(o),u=n(0)("recurly:giftcard")},function(e,t,n){"use strict";function r(e,t){if(e=i({},e),"function"!=typeof t)throw new Error("Missing callback");this.cachedRequest("get","/tax",{country:e.country,postal_code:e.postalCode||e.postal_code,tax_code:e.taxCode||e.tax_code,vat_number:e.vatNumber||e.vat_number},t)}Object.defineProperty(t,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{};b("creating request frame"),r.version=this.recurly.version,r.event=this.name,r.key=this.recurly.config.publicKey,this.once(r.event,function(t){n.relay&&e.document.body.removeChild(n.relay),t.error?n.emit("error",t.error):n.emit("done",t)}),this.url=this.recurly.url(t),this.url+=(~this.url.indexOf("?")?"&":"?")+d.default.stringify(r,{encodeValuesOnly:!0})}},{key:"listen",value:function(t){var n=this;if(this.recurly.bus.add(this),"documentMode"in document){b("Creating relay");var r=document.createElement("iframe");r.width=r.height=0,r.src=this.recurly.url("/relay"),r.name="recurly-relay-"+this.recurly.id+"-"+this.id,r.style.display="none",r.onload=function(){return n.create()},e.document.body.appendChild(r),this.relay=r,b("Created relay",r)}else this.create()}},{key:"create",value:function(){b("opening frame window",this.url,this.name,this.attributes),e.open(this.url,this.name,this.attributes)}},{key:"attributes",get:function(){return"\n resizable,scrollbars,\n width="+this.width+",\n height="+this.height+",\n top="+this.top+",\n left="+this.left+"\n "}},{key:"top",get:function(){var t=e.outerHeight||e.document.documentElement.clientHeight,n=null===e.screenY?e.screenTop:e.screenY;return s(t,this.height,n)}},{key:"left",get:function(){var t=e.outerWidth||e.document.documentElement.clientWidth,n=null===e.screenX?e.screenLeft:e.screenX;return s(t,this.width,n)}}]),n}(h.default)}).call(t,n(3))},function(e,t,n){"use strict";(function(e){function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function u(e){return new j(l({},e,{recurly:this}))}function s(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return{label:e,amount:(0,b.default)(t)}}Object.defineProperty(t,"__esModule",{value:!0});var c=function(){function e(e,t){for(var n=0;n0||(this.config.lineItems.push(this.authorizationLineItem),this.config.total=this.authorizationLineItem.amount)}},{key:"onPricingChange",value:function(){var e=this.config.pricing,t=this.config.lineItems=[];if(this.config.total=e.totalNow,e.hasPrice){var n=e.price.now.taxes||e.price.now.tax;t.push(s(this.config.i18n.subtotalLineItemLabel,e.subtotalPreDiscountNow)),+e.price.now.discount&&t.push(s(this.config.i18n.discountLineItemLabel,-e.price.now.discount)),+n&&t.push(s(this.config.i18n.taxLineItemLabel,n)),+e.price.now.giftCard&&t.push(s(this.config.i18n.giftCardLineItemLabel,-e.price.now.giftCard)),this.config.lineItems=t}}},{key:"onValidateMerchant",value:function(e){var t=this;w("Validating Apple Pay merchant session",e);var n=e.validationURL;this.recurly.request("post","/apple_pay/start",{validationURL:n},function(e,n){if(e)return t.error(e);t.session.completeMerchantValidation(n)})}},{key:"onPaymentMethodSelected",value:function(e){w("Payment method selected",e),this.session.completePaymentMethodSelection(this.finalTotalLineItem,this.lineItems)}},{key:"onShippingContactSelected",value:function(e){var t=this.session.STATUS_SUCCESS,n=[];this.session.completeShippingContactSelection(t,n,this.finalTotalLineItem,this.lineItems)}},{key:"onShippingMethodSelected",value:function(e){this.session.completeShippingMethodSelection(this.finalTotalLineItem,this.lineItems)}},{key:"onPaymentAuthorized",value:function(e){var t=this;w("Payment authorization received",e);var n={};this.config.form&&(n=(0,v.normalize)(g.FIELDS,this.config.form,{parseCard:!1}).values),this.mapPaymentData(n,e.payment),this.recurly.request("post","/apple_pay/token",n,function(e,n){if(e)return t.session.completePayment(t.session.STATUS_FAILURE),t.error("apple-pay-payment-failure",e);w("Token received",n),t.session.completePayment(t.session.STATUS_SUCCESS),t.emit("token",n)})}},{key:"onCancel",value:function(e){w("User canceled Apple Pay payment",e),this.emit("cancel",e)}},{key:"error",value:function(){var e=(arguments.length<=0?void 0:arguments[0])instanceof Error?arguments.length<=0?void 0:arguments[0]:h.default.apply(void 0,arguments);return this.emit("error",e),e}},{key:"mapPaymentData",value:function(e,t){e.paymentData=t.token.paymentData,e.paymentMethod=t.token.paymentMethod,t.billingContact&&(g.FIELDS.some(function(t){return e[t]})||g.FIELDS.forEach(function(n){if(_[n]){var r=t.billingContact[_[n]];"address1"===n?r=r[0]:"address2"===n&&(r=r[1]),e[n]=r}}))}},{key:"session",get:function(){if(this._session)return this._session;w("Creating new Apple Pay session"),this.addAuthorizationLineItem();var t=new e.ApplePaySession(2,{countryCode:this.config.country,currencyCode:this.config.currency,supportedNetworks:this.config.supportedNetworks,merchantCapabilities:this.config.merchantCapabilities,requiredBillingContactFields:["postalAddress"],total:this.totalLineItem});return t.onvalidatemerchant=this.onValidateMerchant.bind(this),t.onshippingcontactselected=this.onShippingContactSelected.bind(this),t.onshippingmethodselected=this.onShippingMethodSelected.bind(this),t.onpaymentmethodselected=this.onPaymentMethodSelected.bind(this),t.onpaymentauthorized=this.onPaymentAuthorized.bind(this),t.oncancel=this.onCancel.bind(this),this._session=t}},{key:"lineItems",get:function(){return[].concat(this.config.lineItems)}},{key:"totalLineItem",get:function(){return s(this.config.label,this.config.total)}},{key:"finalTotalLineItem",get:function(){return l({},this.totalLineItem,{type:"final"})}},{key:"authorizationLineItem",get:function(){return s(this.config.i18n.authorizationLineItemLabel,1)}}]),n}(d.default)}).call(t,n(3))},function(e,t,n){var r=n(63);e.exports=function(e){if("string"==typeof e){var t=e.toUpperCase();if(r.hasOwnProperty(t))return r[t]}},e.exports.currencySymbolMap=r},function(e,t){e.exports={AED:"د.Ø¥",AFN:"Ø‹",ALL:"L",AMD:"֏",ANG:"Æ’",AOA:"Kz",ARS:"$",AUD:"$",AWG:"Æ’",AZN:"ман",BAM:"KM",BBD:"$",BDT:"à§³",BGN:"лв",BHD:".د.ب",BIF:"FBu",BMD:"$",BND:"$",BOB:"$b",BRL:"R$",BSD:"$",BTC:"฿",BTN:"Nu.",BWP:"P",BYR:"p.",BZD:"BZ$",CAD:"$",CDF:"FC",CHF:"CHF",CLP:"$",CNY:"Â¥",COP:"$",CRC:"â‚¡",CUC:"$",CUP:"₱",CVE:"$",CZK:"Kč",DJF:"Fdj",DKK:"kr",DOP:"RD$",DZD:"دج",EEK:"kr",EGP:"£",ERN:"Nfk",ETB:"Br",ETH:"Ξ",EUR:"€",FJD:"$",FKP:"£",GBP:"£",GEL:"₾",GGP:"£",GHC:"₵",GHS:"GH₵",GIP:"£",GMD:"D",GNF:"FG",GTQ:"Q",GYD:"$",HKD:"$",HNL:"L",HRK:"kn",HTG:"G",HUF:"Ft",IDR:"Rp",ILS:"₪",IMP:"£",INR:"₹",IQD:"ع.د",IRR:"ï·¼",ISK:"kr",JEP:"£",JMD:"J$",JOD:"JD",JPY:"Â¥",KES:"KSh",KGS:"лв",KHR:"៛",KMF:"CF",KPW:"â‚©",KRW:"â‚©",KWD:"KD",KYD:"$",KZT:"лв",LAK:"â‚­",LBP:"£",LKR:"₨",LRD:"$",LSL:"M",LTC:"Ł",LTL:"Lt",LVL:"Ls",LYD:"LD",MAD:"MAD",MDL:"lei",MGA:"Ar",MKD:"ден",MMK:"K",MNT:"â‚®",MOP:"MOP$",MUR:"₨",MVR:"Rf",MWK:"MK",MXN:"$",MYR:"RM",MZN:"MT",NAD:"$",NGN:"₦",NIO:"C$",NOK:"kr",NPR:"₨",NZD:"$",OMR:"ï·¼",PAB:"B/.",PEN:"S/.",PGK:"K",PHP:"₱",PKR:"₨",PLN:"zÅ‚",PYG:"Gs",QAR:"ï·¼",RMB:"ï¿¥",RON:"lei",RSD:"Дин.",RUB:"₽",RWF:"Râ‚£",SAR:"ï·¼",SBD:"$",SCR:"₨",SDG:"ج.س.",SEK:"kr",SGD:"$",SHP:"£",SLL:"Le",SOS:"S",SRD:"$",SSP:"£",STD:"Db",SVC:"$",SYP:"£",SZL:"E",THB:"฿",TJS:"SM",TMT:"T",TND:"د.ت",TOP:"T$",TRL:"₤",TRY:"₺",TTD:"TT$",TVD:"$",TWD:"NT$",TZS:"TSh",UAH:"â‚´",UGX:"USh",USD:"$",UYU:"$U",UZS:"лв",VEF:"Bs",VND:"â‚«",VUV:"VT",WST:"WS$",XAF:"FCFA",XBT:"Ƀ",XCD:"$",XOF:"CFA",XPF:"â‚£",YER:"ï·¼",ZAR:"R",ZWD:"Z$"}},function(e,t,n){function r(e){switch({}.toString.call(e)){case"[object Object]":return u(e);case"[object Function]":return e;case"[object String]":return a(e);case"[object RegExp]":return o(e);default:return i(e)}}function i(e){return function(t){return e===t}}function o(e){return function(t){return e.test(t)}}function a(e){return/^ *\W+/.test(e)?new Function("_","return _ "+e):new Function("_","return "+s(e))}function u(e){var t={};for(var n in e)t[n]="string"==typeof e[n]?i(e[n]):r(e[n]);return function(e){if("object"!=typeof e)return!1;for(var n in t){if(!(n in e))return!1;if(!t[n](e[n]))return!1}return!0}}function s(e){var t=l(e);if(!t.length)return"_."+e;var n,r,i;for(r=0;r0&&n(l)?t>1?o(l,t-1,n,r,a):i(a,l):r||(a[a.length]=l)}return a}function a(e,t){return e=Object(e),u(e,t,function(t,n){return n in e})}function u(e,t,n){for(var r=-1,i=t.length,o={};++r-1&&e%1==0&&e<=g}function y(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function v(e){return!!e&&"object"==typeof e}function m(e){return"symbol"==typeof e||v(e)&&E.call(e)==j}var b=1/0,g=9007199254740991,w="[object Arguments]",_="[object Function]",O="[object GeneratorFunction]",j="[object Symbol]",x="object"==typeof t&&t&&t.Object===Object&&t,k="object"==typeof self&&self&&self.Object===Object&&self,C=x||k||Function("return this")(),P=Object.prototype,S=P.hasOwnProperty,E=P.toString,F=C.Symbol,A=P.propertyIsEnumerable,T=F?F.isConcatSpreadable:void 0,M=Math.max,I=Array.isArray,N=function(e,t){return t=M(void 0===t?e.length-1:t,0),function(){for(var r=arguments,i=-1,o=M(r.length-t,0),a=Array(o);++i1&&void 0!==arguments[1]?arguments[1]:function(){},r="https://js.braintreegateway.com/web/"+v+"/js/"+t+".min.js";(0,f.default)(r,function(t){t?e.error("paypal-load-error",{cause:t}):n()})},r=function(){e.braintreeClientAvailable("paypal")?t():n("paypal",t),e.braintreeClientAvailable("dataCollector")?t():n("data-collector",t)};this.braintreeClientAvailable()?r():n("client",r)}},{key:"initialize",value:function(){var t=this;y("Initializing Braintree client");var n=this.config.clientAuthorization,r=e.braintree;r.client.create({authorization:n},function(e,n){if(e)return t.fail("paypal-braintree-api-error",{cause:e});y("Braintree client created"),r.dataCollector.create({client:n,paypal:!0},function(e,i){if(e)return t.fail("paypal-braintree-api-error",{cause:e});y("Device data collector created"),t.deviceFingerprint=i.deviceData,r.paypal.create({client:n},function(e,n){if(e)return t.fail("paypal-braintree-api-error",{cause:e});y("PayPal client created"),t.paypal=n,t.emit("ready")})})})}},{key:"start",value:function(){var e=this,t=u({},this.config.display,{flow:"vault"});this.paypal.tokenize(t,function(t,n){if(t)return"PAYPAL_POPUP_CLOSED"===t.code?e.emit("cancel"):e.error("paypal-braintree-tokenize-braintree-error",{cause:t});y("Token payload received",n),e.deviceFingerprint&&(n.deviceFingerprint=e.deviceFingerprint),e.recurly.request("post","/paypal/token",{type:"braintree",payload:n},function(t,n){if(t)return e.error("paypal-braintree-tokenize-recurly-error",{cause:t});e.emit("token",n)})})}},{key:"braintreeClientAvailable",value:function(t){var n=e.braintree;return n&&n.client&&n.client.VERSION===v&&(!t||t in n)}}]),n}(h.PayPalStrategy)}).call(t,n(3))},function(e,t){function n(e,t){for(var n in t)e.setAttribute(n,t[n])}function r(e,t){e.onload=function(){this.onerror=this.onload=null,t(null,e)},e.onerror=function(){this.onerror=this.onload=null,t(new Error("Failed to load "+this.src),e)}}function i(e,t){e.onreadystatechange=function(){"complete"!=this.readyState&&"loaded"!=this.readyState||(this.onreadystatechange=null,t(null,e))}}e.exports=function(e,t,o){var a=document.head||document.getElementsByTagName("head")[0],u=document.createElement("script");"function"==typeof t&&(o=t,t={}),t=t||{},o=o||function(){},u.type=t.type||"text/javascript",u.charset=t.charset||"utf8",u.async=!("async"in t)||!!t.async,u.src=e,t.attrs&&n(u,t.attrs),t.text&&(u.text=""+t.text),("onload"in u?r:i)(u,o),u.onload||r(u,o),a.appendChild(u)}},function(e,t,n){"use strict";function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=arguments[1],n=0;return function(){return n++&&n===e&&t()}}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function u(e){return new v(l({},e,{recurly:this}))}function s(e){var t=[];return e.skinCode&&8!=e.skinCode.length&&t.push("skinCode should be 8 characters"),e.countryCode&&2!=e.countryCode.length&&t.push("countryCode should be 2 characters"),e.shopperLocale&&5!=e.shopperLocale.length&&t.push("shopperLocale should be 5 characters"),e.invoiceUuid||t.push("invoiceUuid cannot be blank"),y("validate errors",t),t}Object.defineProperty(t,"__esModule",{value:!0});var c=function(){function e(e,t){for(var n=0;n0)return this.error("validation",{fields:r});var i=this.recurly.Frame({height:600,path:"/adyen/start",payload:n});i.once("error",function(e){return t.error("adyen-error",{cause:e})}),i.once("done",function(e){return t.emit("token",e)})}},{key:"error",value:function(){var e=(arguments.length<=0?void 0:arguments[0])instanceof Error?arguments.length<=0?void 0:arguments[0]:h.default.apply(void 0,arguments);return this.emit("error",e),e}}]),t}(d.default)},function(e,t,n){"use strict";(function(e){function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function u(t){var n=e.document.createElement("a");return n.href=t,n.protocol+"//"+n.host}Object.defineProperty(t,"__esModule",{value:!0}),t.Bus=void 0;var s=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};i(this,t);var r=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));return r.id=(0,p.default)(),r.debug=n(0)("recurly:bus:"+r.id),r.emitters=[],r.recipients=[],r.receive=r.receive.bind(r),r.config={api:e.api},r.connect(),r.send=r.send.bind(r),r}return a(t,e),s(t,[{key:"connect",value:function(){window.addEventListener("message",this.receive,!1),this.debug("listening for postMessage events on",window)}},{key:"receive",value:function(e){if(this.originMatches(e.origin))return"string"==typeof e.data?this.receiveFromRelay(e):void(e.data.event&&(this.debug("message received",e.data),this.send(new f.Message(e),null,{exclude:[e.srcElement]})))}},{key:"receiveFromRelay",value:function(e){var t=void 0;try{t=JSON.parse(e.data)}catch(t){this.debug("failed to parse a string message",t,e)}t&&this.send(new f.Message(t.recurly_event,t.recurly_message))}},{key:"add",value:function(e){~this.recipients.indexOf(e)||(this.recipients.push(e),"function"==typeof e.emit&&e.emit("bus:added",this),this.debug("added recipient",e))}},{key:"send",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{exclude:[]},r=new f.Message(e,t);n.exclude;this.debug("sending message to "+this.recipients.length+" recipients",r),this.recipients.forEach(function(e){e.postMessage?"function"==typeof e.postMessage&&e.postMessage(r,"*"):"function"==typeof e.emit&&e.emit(r.event,r.body)})}},{key:"stop",value:function(){this.recipients=[],window.removeEventListener("message",this.receive,!1)}},{key:"originMatches",value:function(e){return u(e)===u(this.config.api)}}]),t}(l.default)}).call(t,n(3))},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{};if(r(this,e),t instanceof e)return t;t instanceof window.MessageEvent&&(n=t.data.body,t=t.data.event),this.event=t,this.body=n}return i(e,[{key:"format",value:function(){return{event:this.event,body:this.body}}}]),e}()},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0}),t.Fraud=void 0;var u=function(){function e(e,t){for(var n=0;n\n \n \n \n ',this.container=this.target.children[0],this.iframe=this.container.querySelector("iframe"),this.window=this.iframe.contentWindow,u()&&(this.tabbingProxy=y.default.createHiddenInput(),this.tabbingProxy.addEventListener("focus",this.focus),this.container.insertBefore(this.tabbingProxy,this.iframe))}},{key:"bindDeferredFocus",value:function(){var t=this;if(this.container.addEventListener("click",this.focus),this.target.id){var n=e.document.querySelectorAll("label[for="+this.target.id+"]");[].slice.apply(n).forEach(function(e){e.addEventListener("click",t.focus)})}}},{key:"reset",value:function(){this.off(),this.target.innerHTML="",delete this.target,delete this.iframe,delete this.window}},{key:"update",value:function(){this.container.className=this.classList,this.iframe.setAttribute("tabindex",this.tabIndex)}},{key:"onReady",value:function(e){e.type===this.type&&(this.ready=!0,this.off("hostedField:ready",this.onReady),this.update())}},{key:"onStateChange",value:function(e){if(e.type===this.type){var t=s({},e);delete t.type,this.state=t,this.update()}}},{key:"onChange",value:function(e){e.type===this.type&&this.update()}},{key:"onConfigure",value:function(e){e.type===this.type&&(this.configure(e),this.update())}},{key:"focus",value:function(){this.bus&&this.bus.send("hostedField:"+this.type+":focus!")}},{key:"type",get:function(){return this.config.type}},{key:"url",get:function(){var e=encodeURIComponent(JSON.stringify(this.config));return this.config.recurly.api+"/field.html#config="+e}},{key:"classList",get:function(){var e="recurly-hosted-field",t=[e];return this.ready&&(t.push(e+"-"+this.config.type),this.state.focus&&(t.push(e+"-focus"),t.push(e+"-"+this.config.type+"-focus")),this.state.valid?(t.push(e+"-valid"),t.push(e+"-"+this.config.type+"-valid")):this.state.focus||this.state.empty||(t.push(e+"-invalid"),t.push(e+"-"+this.config.type+"-invalid"))),t.join(" ")}},{key:"tabIndex",get:function(){var e=parseInt(this.config.tabIndex,10);return isNaN(e)?0:e}}]),n}(f.default)}).call(t,n(3))},function(e,t,n){/*! + * Bowser - a browser detector + * https://github.com/ded/bowser + * MIT License | (c) Dustin Diaz 2015 + */ +!function(t,r,i){void 0!==e&&e.exports?e.exports=i():n(78)("bowser",i)}(0,0,function(){function e(e){function t(t){var n=e.match(t);return n&&n.length>1&&n[1]||""}function n(t){var n=e.match(t);return n&&n.length>1&&n[2]||""}var r,i=t(/(ipod|iphone|ipad)/i).toLowerCase(),o=/like android/i.test(e),u=!o&&/android/i.test(e),s=/nexus\s*[0-6]\s*/i.test(e),c=!s&&/nexus\s*[0-9]+/i.test(e),l=/CrOS/.test(e),f=/silk/i.test(e),d=/sailfish/i.test(e),p=/tizen/i.test(e),h=/(web|hpw)os/i.test(e),y=/windows phone/i.test(e),v=(/SamsungBrowser/i.test(e),!y&&/windows/i.test(e)),m=!i&&!f&&/macintosh/i.test(e),b=!u&&!d&&!p&&!h&&/linux/i.test(e),g=n(/edg([ea]|ios)\/(\d+(\.\d+)?)/i),w=t(/version\/(\d+(\.\d+)?)/i),_=/tablet/i.test(e)&&!/tablet pc/i.test(e),O=!_&&/[^-]mobi/i.test(e),j=/xbox/i.test(e);/opera/i.test(e)?r={name:"Opera",opera:a,version:w||t(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)}:/opr\/|opios/i.test(e)?r={name:"Opera",opera:a,version:t(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i)||w}:/SamsungBrowser/i.test(e)?r={name:"Samsung Internet for Android",samsungBrowser:a,version:w||t(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)}:/coast/i.test(e)?r={name:"Opera Coast",coast:a,version:w||t(/(?:coast)[\s\/](\d+(\.\d+)?)/i)}:/yabrowser/i.test(e)?r={name:"Yandex Browser",yandexbrowser:a,version:w||t(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)}:/ucbrowser/i.test(e)?r={name:"UC Browser",ucbrowser:a,version:t(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)}:/mxios/i.test(e)?r={name:"Maxthon",maxthon:a,version:t(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)}:/epiphany/i.test(e)?r={name:"Epiphany",epiphany:a,version:t(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)}:/puffin/i.test(e)?r={name:"Puffin",puffin:a,version:t(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)}:/sleipnir/i.test(e)?r={name:"Sleipnir",sleipnir:a,version:t(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)}:/k-meleon/i.test(e)?r={name:"K-Meleon",kMeleon:a,version:t(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)}:y?(r={name:"Windows Phone",osname:"Windows Phone",windowsphone:a},g?(r.msedge=a,r.version=g):(r.msie=a,r.version=t(/iemobile\/(\d+(\.\d+)?)/i))):/msie|trident/i.test(e)?r={name:"Internet Explorer",msie:a,version:t(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:l?r={name:"Chrome",osname:"Chrome OS",chromeos:a,chromeBook:a,chrome:a,version:t(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:/edg([ea]|ios)/i.test(e)?r={name:"Microsoft Edge",msedge:a,version:g}:/vivaldi/i.test(e)?r={name:"Vivaldi",vivaldi:a,version:t(/vivaldi\/(\d+(\.\d+)?)/i)||w}:d?r={name:"Sailfish",osname:"Sailfish OS",sailfish:a,version:t(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(e)?r={name:"SeaMonkey",seamonkey:a,version:t(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel|fxios/i.test(e)?(r={name:"Firefox",firefox:a,version:t(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(e)&&(r.firefoxos=a,r.osname="Firefox OS")):f?r={name:"Amazon Silk",silk:a,version:t(/silk\/(\d+(\.\d+)?)/i)}:/phantom/i.test(e)?r={name:"PhantomJS",phantom:a,version:t(/phantomjs\/(\d+(\.\d+)?)/i)}:/slimerjs/i.test(e)?r={name:"SlimerJS",slimer:a,version:t(/slimerjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(e)||/rim\stablet/i.test(e)?r={name:"BlackBerry",osname:"BlackBerry OS",blackberry:a,version:w||t(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:h?(r={name:"WebOS",osname:"WebOS",webos:a,version:w||t(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(e)&&(r.touchpad=a)):/bada/i.test(e)?r={name:"Bada",osname:"Bada",bada:a,version:t(/dolfin\/(\d+(\.\d+)?)/i)}:p?r={name:"Tizen",osname:"Tizen",tizen:a,version:t(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||w}:/qupzilla/i.test(e)?r={name:"QupZilla",qupzilla:a,version:t(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i)||w}:/chromium/i.test(e)?r={name:"Chromium",chromium:a,version:t(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i)||w}:/chrome|crios|crmo/i.test(e)?r={name:"Chrome",chrome:a,version:t(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:u?r={name:"Android",version:w}:/safari|applewebkit/i.test(e)?(r={name:"Safari",safari:a},w&&(r.version=w)):i?(r={name:"iphone"==i?"iPhone":"ipad"==i?"iPad":"iPod"},w&&(r.version=w)):r=/googlebot/i.test(e)?{name:"Googlebot",googlebot:a,version:t(/googlebot\/(\d+(\.\d+))/i)||w}:{name:t(/^(.*)\/(.*) /),version:n(/^(.*)\/(.*) /)},!r.msedge&&/(apple)?webkit/i.test(e)?(/(apple)?webkit\/537\.36/i.test(e)?(r.name=r.name||"Blink",r.blink=a):(r.name=r.name||"Webkit",r.webkit=a),!r.version&&w&&(r.version=w)):!r.opera&&/gecko\//i.test(e)&&(r.name=r.name||"Gecko",r.gecko=a,r.version=r.version||t(/gecko\/(\d+(\.\d+)?)/i)),r.windowsphone||!u&&!r.silk?!r.windowsphone&&i?(r[i]=a,r.ios=a,r.osname="iOS"):m?(r.mac=a,r.osname="macOS"):j?(r.xbox=a,r.osname="Xbox"):v?(r.windows=a,r.osname="Windows"):b&&(r.linux=a,r.osname="Linux"):(r.android=a,r.osname="Android");var x="";r.windows?x=function(e){switch(e){case"NT":return"NT";case"XP":return"XP";case"NT 5.0":return"2000";case"NT 5.1":return"XP";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return}}(t(/Windows ((NT|XP)( \d\d?.\d)?)/i)):r.windowsphone?x=t(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):r.mac?(x=t(/Mac OS X (\d+([_\.\s]\d+)*)/i),x=x.replace(/[_\s]/g,".")):i?(x=t(/os (\d+([_\s]\d+)*) like mac os x/i),x=x.replace(/[_\s]/g,".")):u?x=t(/android[ \/-](\d+(\.\d+)*)/i):r.webos?x=t(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):r.blackberry?x=t(/rim\stablet\sos\s(\d+(\.\d+)*)/i):r.bada?x=t(/bada\/(\d+(\.\d+)*)/i):r.tizen&&(x=t(/tizen[\/\s](\d+(\.\d+)*)/i)),x&&(r.osversion=x);var k=!r.windows&&x.split(".")[0];return _||c||"ipad"==i||u&&(3==k||k>=4&&!O)||r.silk?r.tablet=a:(O||"iphone"==i||"ipod"==i||u||s||r.blackberry||r.webos||r.bada)&&(r.mobile=a),r.msedge||r.msie&&r.version>=10||r.yandexbrowser&&r.version>=15||r.vivaldi&&r.version>=1||r.chrome&&r.version>=20||r.samsungBrowser&&r.version>=4||r.firefox&&r.version>=20||r.safari&&r.version>=6||r.opera&&r.version>=10||r.ios&&r.osversion&&r.osversion.split(".")[0]>=6||r.blackberry&&r.version>=10.1||r.chromium&&r.version>=20?r.a=a:r.msie&&r.version<10||r.chrome&&r.version<20||r.firefox&&r.version<20||r.safari&&r.version<6||r.opera&&r.version<10||r.ios&&r.osversion&&r.osversion.split(".")[0]<6||r.chromium&&r.version<20?r.c=a:r.x=a,r}function t(e){return e.split(".").length}function n(e,t){var n,r=[];if(Array.prototype.map)return Array.prototype.map.call(e,t);for(n=0;n=0;){if(i[0][r]>i[1][r])return 1;if(i[0][r]!==i[1][r])return-1;if(0===r)return 0}}function i(t,n,i){var o=u;"string"==typeof n&&(i=n,n=void 0),void 0===n&&(n=!1),i&&(o=e(i));var a=""+o.version;for(var s in t)if(t.hasOwnProperty(s)&&o[s]){if("string"!=typeof t[s])throw new Error("Browser version in the minVersion map should be a string: "+s+": "+String(t));return r([a,t[s]])<0}return n}function o(e,t,n){return!i(e,t,n)}var a=!0,u=e("undefined"!=typeof navigator?navigator.userAgent||"":"");return u.test=function(e){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:{},n=t.commit,r=void 0===n||n,i=void 0;if(i=(0,v.default)(this.validSubscriptions)?e:(0,h.default)(this.subscriptionCurrencies,e),(0,v.default)(i))throw new Error("unresolvable");if(r)return this.currency(i[0])}},{key:"Calculations",get:function(){return N.default}},{key:"PRICING_METHODS",get:function(){return l(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"PRICING_METHODS",this).concat(["address","adjustment","coupon","currency","giftCard","shippingAddress","subscription","tax"])}},{key:"validAdjustments",get:function(){var e=this;return this.items.adjustments.filter(function(t){return t.currency===e.items.currency})}},{key:"validSubscriptions",get:function(){return this.items.subscriptions.filter(function(e){return e.isValid})}},{key:"subscriptionCurrencies",get:function(){return(0,h.default)(this.validSubscriptions.map(function(e){return Object.keys(e.items.plan.price)}))}},{key:"subscriptionPlanCodes",get:function(){return(0,O.default)(this.validSubscriptions.map(function(e){return e.items.plan.code}))}}]),t}(P.Pricing);t.default=q},function(e,t){function n(e){var t={},n=e.length-1,r=e[0],i=e[n];for(var o in r)t[r[o]]=0;for(var o=1;o<=n;o++){var a=e[o];for(var u in a){var s=a[u];t[s]===o-1&&(t[s]=o)}}var c=[];for(var o in i){var s=i[o];t[s]===n&&c.push(s)}return c}function r(e,t){if(!t)return n(e);for(var r=[],o=0;o-1&&r.push(e[o]);return r}function i(e,t){for(var n=0;ne?(n=e,r=t-e):n=t,{used:n,remains:r}}if(this.pricing.items.gift_card){var t=this.price.now.total,n=this.price.next.total,r=e(t,this.pricing.items.gift_card.unit_amount),i=e(n,r.remains);this.price.now.gift_card=r.used,this.price.next.gift_card=i.used,this.price.now.total=t-r.used,this.price.next.total=n-i.used}}},{key:"planPrice",value:function(){var e=this.items.plan;if(!e)return{amount:0,setup_fee:0};var t=e.price[this.items.currency];return t.amount=t.unit_amount*(e.quantity||1),t}},{key:"isTrial",value:function(){var e=this.items.coupon;return!(!e||"free_trial"!==e.discount.type)||this.items.plan.trial}}]),e}();t.default=m},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){return function(){return new l.default(function(e,t){return e()},t)}}Object.defineProperty(t,"__esModule",{value:!0}),t.DEFERRED_METHODS=t.DISABLED_METHODS=void 0;var a=function(){function e(e,t){for(var n=0;n1)try{t.checkout.resolveCurrency(Object.keys(o.price),{commit:!1})}catch(r){return n((0,s.default)("invalid-plan-currency",{planCode:o.code,currencies:t.checkout.subscriptionCurrencies}))}o.quantity=i,t.subscription.items.plan=o;var a=function(){t.subscription.emit("set.plan",o),e(o)};if(t.checkout.currencyCode in o.price)a();else try{t.checkout.resolveCurrency(Object.keys(o.price)).then(a)}catch(e){n(r)}})},this.subscription).nodeify(a)}},{key:"id",get:function(){return this.subscription.id}},{key:"isValid",get:function(){return this.subscription.isValid}},{key:"items",get:function(){return this.subscription.items}},{key:"price",get:function(){return this.subscription.price}},{key:"taxCode",get:function(){return this.subscription.taxCode}},{key:"taxExempt",get:function(){return this.subscription.taxExempt}}]),e}();t.default=h},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){var n=t.id,r=t.price;return{type:"subscription",id:n,amount:r[e].total,setupFee:r[e].setup_fee,addons:r[e].addons,plan:r[e].plan}}function a(e){if(!e.items.plan.trial)return 0;var t={days:86400,months:2678400}[e.items.plan.trial.interval]||0;return e.items.plan.trial.length*t}function u(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.setupFees,i=void 0===r||r,o=t.price[e],a=parseFloat(o.subtotal)+parseFloat(o.discount);return i||(a-=parseFloat(o.setup_fee)),a}function s(e,t){return 0===e.length?e:t.applies_to_all_plans?e:e.filter(function(e){return e.couponIsValidForSubscription(t)})}function c(e){return parseFloat((Math.round(100*e)/100).toFixed(6))}Object.defineProperty(t,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;te?(n=e,r=t-e):n=t,{used:n,remains:r}}if(this.price.now.giftCard=0,this.price.next.giftCard=0,this.items.giftCard){var t=this.price.now.subtotal+this.price.now.taxes,n=this.price.next.subtotal+this.price.next.taxes,r=e(t,this.items.giftCard.unit_amount),i=r.used,o=r.remains,a=e(n,o),u=a.used;return this.price.now.giftCard=i,this.price.next.giftCard=u,m.default.resolve()}}},{key:"totals",value:function(){return this.price.now.total=this.price.now.subtotal+this.price.now.taxes-this.price.now.giftCard,this.price.next.total=this.price.next.subtotal+this.price.next.taxes-this.price.next.giftCard,m.default.resolve()}},{key:"itemizedSets",value:function(){return this.price.now.items=this._itemizedSets.now.subscriptions.concat(this._itemizedSets.now.adjustments),this.price.next.items=this._itemizedSets.next.subscriptions,m.default.resolve()}},{key:"applyFreeTrialCoupon",value:function(){var e=this,t=this.items.coupon;return this.hasValidSubscriptions?"subscription"===t.redemption_resource?this.mostValuableSubscriptionForFreeTrial().coupon(t).reprice(null,{internal:!0}).then(function(){return e.subscriptions()}):m.default.all(this.validSubscriptions.map(function(e){return e.coupon(t).reprice(null,{internal:!0})})).then(function(){return e.subscriptions()}):m.default.resolve()}},{key:"discountAmounts",value:function(){var e=(arguments.length>0&&void 0!==arguments[0]&&arguments[0],this.items.coupon),t=0,n=0;if(e)if("free_trial"===e.discount.type);else if(e.discount.rate){var r=this.discountableSubtotals(e,{setupFees:!1}),i=r.discountableNow,o=r.discountableNext;t=c(i*e.discount.rate),n=c(o*e.discount.rate)}else if(e.discount.amount){var a=this.discountableSubtotals(e),u=a.discountableNow,s=a.discountableNext,l=e.discount.amount[this.items.currency]||0;t=Math.min(u,l),n=Math.min(s,l)}return{discountNow:t,discountNext:n}}},{key:"discountableSubtotals",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.setupFees,r=void 0===n||n,i=t.taxExempt,o=void 0===i||i,a=0,c=0;if(e.applies_to_non_plan_charges&&(a+=o?this.price.now.adjustments:this.validAdjustments.reduce(function(e,t){return e+(t.taxExempt?0:t.amount*t.quantity)},0)),e.applies_to_plans&&this.hasValidSubscriptions){var l=void 0;l="subscription"===e.redemption_resource?[this.mostValuableSubscriptionForDiscount()]:this.validSubscriptions,l=s(l,e),l.forEach(function(e){!o&&e.taxExempt||(a+=u("now",e,{setupFees:r}),c+=u("next",e,{setupFees:r}))})}return{discountableNow:a,discountableNext:c}}},{key:"mostValuableSubscriptionForDiscount",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.validSubscriptions;return e=s(e,this.items.coupon),e.sort(function(e,t){var n=parseFloat(e.price.now.subtotal),r=parseFloat(t.price.now.subtotal);return n>r?-1:n0}},{key:"taxableAdjustments",get:function(){return this.validAdjustments.filter(function(e){return!e.taxExempt&&e.amount>0})}},{key:"taxableSubscriptions",get:function(){return this.validSubscriptions.filter(function(e){return!e.items.plan.tax_exempt})}},{key:"taxCodes",get:function(){var e=this.taxableAdjustments.concat(this.taxableSubscriptions);return(0,g.default)(e.map(function(e){return e.taxCode}))}}]),e}();t.default=C},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n19)return!1;for(;o--;)n=parseInt(t.charAt(o),10)*i,r+=n-9*(n>9),i^=3;return r%10==0&&r>0}function i(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=(0,c.parseCard)(e),r=n.length,i=(0,s.default)(l,function(e){return e.pattern.test(n)&&(t||~e.lengths.indexOf(r))});return i&&i.type||"unknown"}function o(e,t){if((e=Number(e)-1)<0||e>11)return!1;t=Number(t),t+=t<100?2e3:0;var n=new Date;return n.setYear(t),n.setDate(1),n.setHours(0),n.setMinutes(0),n.setSeconds(0),n.setMonth(e+1),new Date=222100&&t<=272099}},lengths:[16]},{type:"american_express",pattern:/^3[47]/,lengths:[15]},{type:"visa",pattern:/^4/,lengths:[13,16]},{type:"jcb",pattern:/^35[2-8]\d/,lengths:[16]},{type:"diners_club",pattern:/^(30[0-5]|309|36|3[89]|54|55|2014|2149)/,lengths:[14]}]}]); \ No newline at end of file diff --git a/services/web/public/js/libs/recurly.min.js b/services/web/public/js/libs/recurly.min.js deleted file mode 100755 index 8d1833a20c..0000000000 --- a/services/web/public/js/libs/recurly.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(f){function v(D){function C(){}C.prototype=D||this;return new C()}var h={};h.settings={enableGeoIP:true,acceptedCards:["visa","mastercard","discover","american_express"],oneErrorPerField:true};h.version="2.2.9";h.dom={};h.Error={toString:function(){return"RecurlyJS Error: "+this.message}};h.raiseError=function(C){var D=v(h.Error);D.message=C;throw D};h.config=function(C){f.extend(true,h.settings,C);if(!C.baseURL){var D=h.settings.subdomain||h.raiseError("company subdomain not configured");h.settings.baseURL="https://"+D+".recurly.com/jsonp/"+D+"/"}};function u(D,C){if(D==1){return C.substr(0,C.length-1)}return""+D+" "+C}(h.Cost=function(C){this._cents=C||0}).prototype={toString:function(){return h.formatCurrency(this.dollars())},cents:function(C){if(C===undefined){return this._cents}return new h.Cost(C)},dollars:function(C){if(C===undefined){return this._cents/100}return new h.Cost(C*100)},mult:function(C){return new h.Cost(this._cents*C)},add:function(C){if(C.cents){C=C.cents()}return new h.Cost(this._cents+C)},sub:function(C){if(C.cents){C=C.cents()}return new h.Cost(this._cents-C)}};h.Cost.FREE=new h.Cost(0);(h.TimePeriod=function(D,C){this.length=D;this.unit=C}).prototype={toString:function(){return""+u(this.length,this.unit)},toDate:function(){var C=new Date();switch(this.unit){case"month":C.setMonth(C.getMonth()+this.length);break;case"day":C.setDay(C.getDay()+this.length);break}return C},clone:function(){return new h.TimePeriod(this.length,this.unit)}};(h.RecurringCost=function(D,C){this.cost=D;this.interval=C}).prototype={toString:function(){return""+this.cost+" every "+this.interval},clone:function(){return new h.TimePeriod(this.length,this.unit)}};h.RecurringCost.FREE=new h.RecurringCost(0,null);(h.RecurringCostStage=function(C,D){this.recurringCost=C;this.duration=D}).prototype={toString:function(){this.recurringCost.toString()+" for "+this.duration.toString()}};h.locale={};h.locale.errors={emptyField:"Required field",missingFullAddress:"Please enter your full address.",invalidEmail:"Invalid",invalidCC:"Invalid",invalidCVV:"Invalid",invalidCoupon:"Invalid",cardDeclined:"Transaction declined",acceptTOS:"Please accept the Terms of Service.",invalidQuantity:"Invalid quantity"};h.locale.currencies={};h.locale.currency={format:"%u%n",separator:".",delimiter:",",precision:2};function s(C,D){var F=h.locale.currencies[C]=v(h.locale.currency);for(var E in D){F[E]=D[E]}}s("USD",{symbol:"$"});s("AUD",{symbol:"$"});s("CAD",{symbol:"$"});s("EUR",{symbol:"\u20ac"});s("GBP",{symbol:"\u00a3"});s("CZK",{symbol:"\u004b"});s("DKK",{symbol:"\u006b\u0072"});s("HUF",{symbol:"Ft"});s("JPY",{symbol:"\u00a5"});s("NOK",{symbol:"kr"});s("NZD",{symbol:"$"});s("PLN",{symbol:"\u007a"});s("SGD",{symbol:"$"});s("SEK",{symbol:"kr"});s("CHF",{symbol:"Fr"});s("ZAR",{symbol:"R"});h.settings.locale=h.locale;h.knownCards={visa:{prefixes:[4],name:"Visa"},mastercard:{prefixes:[51,52,53,54,55],name:"MasterCard"},american_express:{prefixes:[34,37],name:"American Express"},discover:{prefixes:[6011,62,64,65],name:"Discover"},diners_club:{prefixes:[305,36,38],name:"Diners Club"},carte_blanche:{prefixes:[300,301,302,303,304,305]},jcb:{prefixes:[35],name:"JCB"},enroute:{prefixes:[2014,2149],name:"EnRoute"},maestro:{prefixes:[5018,5020,5038,6304,6759,6761],name:"Maestro"},laser:{prefixes:[6304,6706,6771,6709],name:"Laser"}};h.detectCardType=function(E){E=E.replace(/\D/g,"");var H=h.knownCards;for(var C in H){if(H.hasOwnProperty(C)){var I=H[C];for(var G=0,D=I.prefixes.length;G").hide();F.attr("action",D).attr("method","POST").attr("enctype","application/x-www-form-urlencoded");f('').attr({name:"recurly_token",value:E}).appendTo(F);f("body").append(F);F.submit()};function k(F){var E=f("
');if(!P.displayQuantity){J.find(".quantity").remove()}J.data("add_on",P);J.appendTo(K)}K.delegate(".quantity input","change keyup recalculate",function(ac){var Y=f(this);var Z=Y.closest(".add_on");var ab=Z.data("add_on");var aa=Y.val()===""?1:parseInt(Y.val(),10);V.findAddOnByCode(ab.code).quantity=aa>0?aa:0;M()});K.delegate(".quantity input","blur",function(ab){var Y=f(this);var Z=Y.closest(".add_on");var aa=parseInt(Y.val(),10);if(aa<1){Y.trigger("recalculate")}if(aa===0){Z.trigger("actuate")}});K.bind("selectstart",function(Y){if(f(Y.target).is(".add_on")){Y.preventDefault()}});K.delegate(".add_on","click actuate",function(ac){if(f(ac.target).closest(".quantity").length){return}var aa=!f(this).hasClass("selected");f(this).toggleClass("selected",aa);var ab=f(this).data("add_on");if(aa){var Y=V.redeemAddOn(ab);var Z=f(this).find(".quantity input");var ad=parseInt(Z.val(),10);if(ad<1||isNaN(ad)){ad=1;Z.val(ad)}Y.quantity=ad;Z.focus()}else{V.removeAddOn(ab.code)}M()});K.find("input").trigger("init")}}else{K.remove()}var N=C.find(".coupon");var R=null;function W(){var Y=N.find("input").val();if(Y==R){return}R=Y;if(!Y){N.removeClass("invalid").removeClass("valid");N.find(".description").text("");V.coupon=undefined;M();return}N.addClass("checking");V.getCoupon(Y,function(Z){N.removeClass("checking");V.coupon=Z;N.removeClass("invalid").addClass("valid");N.find(".description").text(Z.description);M()},function(){V.coupon=undefined;N.removeClass("checking");N.removeClass("valid").addClass("invalid");N.find(".description").text(h.locale.errors.invalidCoupon);M()})}if(D.enableCoupons){N.find("input").bind("keyup change",function(Y){});N.find("input").keypress(function(Y){if(Y.charCode==13){Y.preventDefault();W()}});N.find(".check").click(function(){W()});N.find("input").blur(function(){N.find(".check").click()})}else{N.remove()}var I=C.find(".vat");var X=C.find(".vat_number");var Q=X.find("input");I.find(".title").text("VAT at "+h.settings.VATPercent+"%");function L(){var Z=C.find(".country select").val();var ab=h.isVATNumberApplicable(Z);X.toggleClass("applicable",ab);X.toggleClass("inapplicable",!ab);var aa=Q.val();var Y=h.isVATChargeApplicable(Z,aa);I.toggleClass("applicable",Y);I.toggleClass("inapplicable",!Y)}C.find(".country select").change(function(){O.country=f(this).val();M();L()}).change();Q.bind("keyup change",function(){O.vatNumber=f(this).val();M();L()});C.submit(function(Y){Y.preventDefault();a(C);C.find(".error").remove();C.find(".invalid").removeClass("invalid");B(function(Z){q(C,V.plan,D,Z);i(C,H,D,Z);d(C,O,D,Z);y(C,Z)},function(){C.addClass("submitting");var Z=C.find("button.submit").text();C.find("button.submit").attr("disabled",true).text("Please Wait");V.save({signature:D.signature,success:function(aa){if(D.successHandler){D.successHandler(h.getToken(aa))}if(D.successURL){var ab=D.successURL;h.postResult(ab,aa,D)}},error:function(aa){if(!D.onError||!D.onError(aa)){p(C,aa)}},complete:function(){C.removeClass("submitting");C.find("button.submit").removeAttr("disabled").text(Z)}})})});W();M();if(D.beforeInject){D.beforeInject(C.get(0))}f(function(){var Y=f(D.target);Y.html(C);if(D.afterInject){D.afterInject(C.get(0))}})}};h.paypal={start:function(G){var J=window.name;if(J.indexOf("recurly_result")>-1){window.name="";J=""}var H=f.extend(G.data,{post_message:true,referer:window.location.href}),E=G.url+"?"+f.param(H),C=window.open(E,"recurly_paypal","menubar=1,resizable=1");window.popup=C;f(window).on("message",I);var D=setInterval(function(){var L=decodeURIComponent(window.name),M=L.match(/recurly_result=(.*)[&$]?/),K=M&&f.parseJSON(M[1]);if(K){F(K);window.name=J}},1000);function F(K){try{C.close()}finally{G.success(K);G.complete();f(window).unbind("message",I);clearInterval(D)}}function I(M){var L=document.createElement("a");L.href=h.settings.baseURL;var K=L.protocol+"//"+L.host.replace(/:\d+$/,"");if(M.originalEvent.origin==K){F(M.originalEvent.data)}}}};h.states={};h.states.US={"-":"Select State","--":"------------",AL:"Alabama",AK:"Alaska",AS:"American Samoa",AZ:"Arizona",AR:"Arkansas",AA:"Armed Forces Americas",AE:"Armed Forces Europe, Middle East, & Canada",AP:"Armed Forces Pacific",CA:"California",CO:"Colorado",CT:"Connecticut",DE:"Delaware",DC:"District of Columbia",FM:"Federated States of Micronesia",FL:"Florida",GA:"Georgia",GU:"Guam",HI:"Hawaii",ID:"Idaho",IL:"Illinois",IN:"Indiana",IA:"Iowa",KS:"Kansas",KY:"Kentucky",LA:"Louisiana",ME:"Maine",MH:"Marshall Islands",MD:"Maryland",MA:"Massachusetts",MI:"Michigan",MN:"Minnesota",MS:"Mississippi",MO:"Missouri",MT:"Montana",NE:"Nebraska",NV:"Nevada",NH:"New Hampshire",NJ:"New Jersey",NM:"New Mexico",NY:"New York",NC:"North Carolina",ND:"North Dakota",MP:"Northern Mariana Islands",OH:"Ohio",OK:"Oklahoma",OR:"Oregon",PW:"Palau",PA:"Pennsylvania",PR:"Puerto Rico",RI:"Rhode Island",SC:"South Carolina",SD:"South Dakota",TN:"Tennessee",TX:"Texas",UT:"Utah",VT:"Vermont",VI:"Virgin Islands",VA:"Virginia",WA:"Washington",WV:"West Virginia",WI:"Wisconsin",WY:"Wyoming"};h.states.CA={"-":"Select State","--":"------------",AB:"Alberta",BC:"British Columbia",MB:"Manitoba",NB:"New Brunswick",NL:"Newfoundland",NT:"Northwest Territories",NS:"Nova Scotia",NU:"Nunavut",ON:"Ontario",PE:"Prince Edward Island",QC:"Quebec",SK:"Saskatchewan",YT:"Yukon Territory"};h.dom.contact_info_fields='
Contact Info
First Name
Last Name
Phone Number
Company/Organization Name
';h.dom.billing_info_fields='
Billing Info
First Name
Last Name
Credit Card Number
CVV
Expires
Address
Apt/Suite
City
State/Province
Zip/Postal
VAT Number
You will be taken to PayPal to authorize a billing agreement.

Please make sure pop-ups aren\'t blocked.
';h.dom.subscribe_form='';h.dom.update_billing_info_form='
';h.dom.one_time_transaction_form='
';h.dom.terms_of_service='
Terms of Service and Privacy Policy';window.Recurly=h})(jQuery); \ No newline at end of file From 06fb31533fd68809cfa308db6ff68303c1c9dc42 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:26:39 +0100 Subject: [PATCH 41/47] add highlight for bad card/cvv/mmyy number in. --- services/web/app/views/subscriptions/new.pug | 8 ++------ services/web/public/stylesheets/components/forms.less | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 5644710723..3755f359e7 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -19,7 +19,7 @@ block content .page-header .row .col-xs-9 - h2 {{planName}} + h2 {{planName}} {{validation.errorFields}} {{validation.errorFields.number}} .col-xs-3 div.dropdown.changePlanButton.pull-right(ng-cloak, dropdown) a.btn.btn-default.dropdown-toggle( @@ -102,13 +102,12 @@ block content required ) - .form-group() + .form-group(ng-class="validation.errorFields.number ? 'has-error' : ''") label(for="card-no") #{translate("credit_card_number")} div#card-no( type="text" name="ccNumber" data-recurly='number' - ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;" ng-blur="validateCardNumber();" required ) @@ -120,7 +119,6 @@ block content div( type="number" name="month" - ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" ng-blur="updateExpiry(); validateExpiry()" data-recurly="month" required @@ -132,7 +130,6 @@ block content type="number" name="year" data-recurly="year" - ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" ng-blur="updateExpiry(); validateExpiry()" required ) @@ -143,7 +140,6 @@ block content div( type="number" ng-model="data.cvv" - ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" ng-blur="validateCvv()" data-recurly="cvv" name="cvv" diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index 4e32779d7b..8202be7c3f 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -311,6 +311,7 @@ input[type="checkbox"], } .has-error { .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); + color:@red; } .form-control.ng-dirty.ng-invalid:not(:focus) { From 7940b60144acab64fff68d1c0a241e98e6116a7a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:35:12 +0100 Subject: [PATCH 42/47] make a seperate external error class which is used on entire payment form --- services/web/app/views/subscriptions/new.pug | 16 ++++++++-------- .../web/public/stylesheets/components/forms.less | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 3755f359e7..4b824e5739 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -19,7 +19,7 @@ block content .page-header .row .col-xs-9 - h2 {{planName}} {{validation.errorFields}} {{validation.errorFields.number}} + h2 {{planName}} .col-xs-3 div.dropdown.changePlanButton.pull-right(ng-cloak, dropdown) a.btn.btn-default.dropdown-toggle( @@ -79,7 +79,7 @@ block content div(ng-show="paymentMethod.value === 'credit_card'") .row .col-xs-6 - .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") + .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-external-error' : ''") label(for="first-name") #{translate('first_name')} input#first-name.form-control( type="text" @@ -91,7 +91,7 @@ block content ) span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }} .col-xs-6 - .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''") + .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-external-error' : ''") label(for="last-name") #{translate('last_name')} input#last-name.form-control( type="text" @@ -102,7 +102,7 @@ block content required ) - .form-group(ng-class="validation.errorFields.number ? 'has-error' : ''") + .form-group(ng-class="validation.errorFields.number ? 'has-external-error' : ''") label(for="card-no") #{translate("credit_card_number")} div#card-no( type="text" @@ -114,7 +114,7 @@ block content .row .col-xs-3 - .form-group.has-feedback() + .form-group.has-feedback(ng-class="validation.errorFields.month ? 'has-external-error' : ''") label(for="month") #{translate("month")} div( type="number" @@ -124,7 +124,7 @@ block content required ) .col-xs-3 - .form-group.has-feedback() + .form-group.has-feedback(ng-class="validation.errorFields.year ? 'has-external-error' : ''") label(for="year") #{translate("year")} div( type="number" @@ -135,7 +135,7 @@ block content ) .col-xs-6 - .form-group.has-feedback() + .form-group.has-feedback(ng-class="validation.errorFields.cvv ? 'has-external-error' : ''") label #{translate("security_code")} div( type="number" @@ -156,7 +156,7 @@ block content ) ? div - .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") + .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-external-error' : ''") label(for="country") #{translate('country')} select#country.form-control( data-recurly="country" diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index 8202be7c3f..de13f1d48f 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -311,6 +311,9 @@ input[type="checkbox"], } .has-error { .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); +} +.has-external-error { + .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); color:@red; } From c3eeefdc5b91b68f9076c64870cb88e77be4d013 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:52:33 +0100 Subject: [PATCH 43/47] only use recurly.js from our public libs folder --- services/web/app/views/subscriptions/new.pug | 2 -- services/web/public/coffee/main/new-subscription.coffee | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 4b824e5739..a29c03903b 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -1,8 +1,6 @@ extends ../layout block scripts - script(src="https://js.recurly.com/v4/recurly.js") - script(type='text/javascript'). window.countryCode = '#{countryCode}' window.plan_code = '#{plan_code}' diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index ab1128ed33..02939a3b9c 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -1,6 +1,7 @@ define [ "base", "directives/creditCards" + "libs/recurly-4.8.5" ], (App)-> App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)-> From 985d05a2975c60eab8d013c847552ded25ae17c4 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 10:56:03 +0100 Subject: [PATCH 44/47] remove .has-error class, not used --- services/web/public/stylesheets/components/forms.less | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index de13f1d48f..8eb92411cd 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -309,9 +309,6 @@ input[type="checkbox"], .has-warning { .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg); } -.has-error { - .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); -} .has-external-error { .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); color:@red; From 086f080ee4bd54175fcfe73165656a289fe19915 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 11:01:14 +0100 Subject: [PATCH 45/47] add `!isFormValid(simpleCCForm)` back in, was used during debugging --- services/web/app/views/subscriptions/new.pug | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index a29c03903b..08aefd93af 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -106,8 +106,6 @@ block content type="text" name="ccNumber" data-recurly='number' - ng-blur="validateCardNumber();" - required ) .row @@ -117,9 +115,7 @@ block content div( type="number" name="month" - ng-blur="updateExpiry(); validateExpiry()" data-recurly="month" - required ) .col-xs-3 .form-group.has-feedback(ng-class="validation.errorFields.year ? 'has-external-error' : ''") @@ -128,8 +124,6 @@ block content type="number" name="year" data-recurly="year" - ng-blur="updateExpiry(); validateExpiry()" - required ) .col-xs-6 @@ -138,10 +132,8 @@ block content div( type="number" ng-model="data.cvv" - ng-blur="validateCvv()" data-recurly="cvv" name="cvv" - required cc-format-sec-code ) .form-control-feedback @@ -197,7 +189,7 @@ block content div.payment-submit button.btn.btn-success.btn-block( ng-click="submit()" - ng-disabled="processing" + ng-disabled="processing || !isFormValid(simpleCCForm);" ) span(ng-show="processing") i.fa.fa-spinner.fa-spin From 39e41681a0f98f4ce9e5815c9e1ce21cad65ebe0 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 16 Apr 2018 12:19:55 +0100 Subject: [PATCH 46/47] change to plans[currencyCode]['symbol'] from price.currency.symbol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit price.currency.symbol returns bad chars. '£12.00' and '€14.40' for eur --- services/web/app/views/subscriptions/new.pug | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug index 08aefd93af..6e9d473307 100644 --- a/services/web/app/views/subscriptions/new.pug +++ b/services/web/app/views/subscriptions/new.pug @@ -39,11 +39,11 @@ block content span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})} span(ng-if="discountMonths && discountRate")   - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off div(ng-if="price") - strong {{price.currency.symbol}}{{price.next.total}} + strong {{plans[currencyCode]['symbol']}}{{price.next.total}} span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")} span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")} div(ng-if="normalPrice") - span.small Normally {{price.currency.symbol}}{{normalPrice}} + span.small Normally {{plans[currencyCode]['symbol']}}{{normalPrice}} .row div() .col-md-12() @@ -180,8 +180,8 @@ block content div.price-breakdown(ng-if="price.next.tax !== '0.00'") hr.thin span Total: - strong {{price.currency.symbol}}{{price.next.total}} - span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax) + strong {{plans[currencyCode]['symbol']}}{{price.next.total}} + span ({{plans[currencyCode]['symbol']}}{{price.next.subtotal}} + {{plans[currencyCode]['symbol']}}{{price.next.tax}} tax) span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")} span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")} hr.thin From 3af10733cc19b8030bc7d0537ecae0aa9b7788c0 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 11 Apr 2018 12:36:47 +0100 Subject: [PATCH 47/47] Replace hard coded rich text hashed file path with registration system --- .../web/app/coffee/infrastructure/ExpressLocals.coffee | 4 ++-- services/web/app/coffee/infrastructure/Modules.coffee | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 0855a35c74..37cce62ddd 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -15,6 +15,7 @@ htmlEncoder = new require("node-html-encoder").Encoder("numerical") hashedFiles = {} Path = require 'path' Features = require "./Features" +Modules = require "./Modules" jsPath = if Settings.useMinifiedJs @@ -41,10 +42,9 @@ pathList = [ "#{jsPath}ide.js" "#{jsPath}main.js" "#{jsPath}libraries.js" - "#{jsPath}es/rich-text.js" "/stylesheets/style.css" "/stylesheets/ol-style.css" -] +].concat(Modules.moduleAssetFiles(jsPath)) if !Settings.useMinifiedJs logger.log "not using minified JS, not hashing static files" diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee index 9d4da24c8b..769182ad94 100644 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ b/services/web/app/coffee/infrastructure/Modules.coffee @@ -43,6 +43,13 @@ module.exports = Modules = moduleIncludesAvailable: (view) -> return (Modules.viewIncludes[view] or []).length > 0 + moduleAssetFiles: (pathPrefix) -> + assetFiles = [] + for module in @modules + for assetFile in module.assetFiles or [] + assetFiles.push "#{pathPrefix}#{assetFile}" + return assetFiles + attachHooks: () -> for module in @modules if module.hooks?