mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #9466 from overleaf/jk-add-labs-program-for-galileo
[web] Add a new "Overleaf Labs" program, for Galileo GitOrigin-RevId: 8f6c79c37c1719a59bd8405998cc3de2fd29960d
This commit is contained in:
parent
8388d808a5
commit
73e8fd115b
18 changed files with 388 additions and 252 deletions
260
services/web/app/src/Features/Newsletter/MailChimpProvider.js
Normal file
260
services/web/app/src/Features/Newsletter/MailChimpProvider.js
Normal file
|
@ -0,0 +1,260 @@
|
|||
const logger = require('@overleaf/logger')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const crypto = require('crypto')
|
||||
const Mailchimp = require('mailchimp-api-v3')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { callbackify } = require('util')
|
||||
|
||||
function mailchimpIsConfigured() {
|
||||
return Settings.mailchimp != null && Settings.mailchimp.api_key != null
|
||||
}
|
||||
|
||||
function make(listName, listId) {
|
||||
let provider
|
||||
if (!mailchimpIsConfigured() || !listId) {
|
||||
logger.debug({ listName }, 'Using newsletter provider: none')
|
||||
provider = makeNullProvider(listName)
|
||||
} else {
|
||||
logger.debug({ listName }, 'Using newsletter provider: mailchimp')
|
||||
provider = makeMailchimpProvider(listName, listId)
|
||||
}
|
||||
return {
|
||||
subscribed: callbackify(provider.subscribed),
|
||||
subscribe: callbackify(provider.subscribe),
|
||||
unsubscribe: callbackify(provider.unsubscribe),
|
||||
changeEmail: callbackify(provider.changeEmail),
|
||||
promises: provider,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
make,
|
||||
}
|
||||
|
||||
class NonFatalEmailUpdateError extends OError {
|
||||
constructor(message, oldEmail, newEmail) {
|
||||
super(message, { oldEmail, newEmail })
|
||||
}
|
||||
}
|
||||
|
||||
function makeMailchimpProvider(listName, listId) {
|
||||
const mailchimp = new Mailchimp(Settings.mailchimp.api_key)
|
||||
const MAILCHIMP_LIST_ID = listId
|
||||
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
const result = await mailchimp.get(path)
|
||||
return result?.status === 'subscribed'
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return false
|
||||
}
|
||||
throw OError.tag(err, 'error getting newsletter subscriptions status', {
|
||||
userId: user._id,
|
||||
listName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
await mailchimp.put(path, {
|
||||
email_address: user.email,
|
||||
status: 'subscribed',
|
||||
status_if_new: 'subscribed',
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
logger.debug(
|
||||
{ user, listName },
|
||||
'finished subscribing user to newsletter'
|
||||
)
|
||||
} catch (err) {
|
||||
throw OError.tag(err, 'error subscribing user to newsletter', {
|
||||
userId: user._id,
|
||||
listName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribe(user, options = {}) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
if (options.delete) {
|
||||
await mailchimp.delete(path)
|
||||
} else {
|
||||
await mailchimp.patch(path, {
|
||||
status: 'unsubscribed',
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
}
|
||||
logger.debug(
|
||||
{ user, options, listName },
|
||||
'finished unsubscribing user from newsletter'
|
||||
)
|
||||
} catch (err) {
|
||||
if (err.status === 404 || err.status === 405) {
|
||||
// silently ignore users who were never subscribed (404) or previously deleted (405)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.message.includes('looks fake or invalid')) {
|
||||
logger.debug(
|
||||
{ err, user, options, listName },
|
||||
'Mailchimp declined to unsubscribe user because it finds the email looks fake'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
throw OError.tag(err, 'error unsubscribing user from newsletter', {
|
||||
userId: user._id,
|
||||
listName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function changeEmail(user, newEmail) {
|
||||
const oldEmail = user.email
|
||||
|
||||
try {
|
||||
await updateEmailInMailchimp(user, newEmail)
|
||||
} catch (updateError) {
|
||||
// if we failed to update the user, delete their old email address so that
|
||||
// we don't leave it stuck in mailchimp
|
||||
logger.debug(
|
||||
{ oldEmail, newEmail, updateError, listName },
|
||||
'unable to change email in newsletter, removing old mail'
|
||||
)
|
||||
|
||||
try {
|
||||
await unsubscribe(user, { delete: true })
|
||||
} catch (unsubscribeError) {
|
||||
// something went wrong removing the user's address
|
||||
throw OError.tag(
|
||||
unsubscribeError,
|
||||
'error unsubscribing old email in response to email change failure',
|
||||
{ oldEmail, newEmail, updateError, listName }
|
||||
)
|
||||
}
|
||||
|
||||
if (!(updateError instanceof NonFatalEmailUpdateError)) {
|
||||
throw updateError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEmailInMailchimp(user, newEmail) {
|
||||
const oldEmail = user.email
|
||||
|
||||
// mailchimp doesn't give us error codes, so we have to parse the message :'(
|
||||
const errors = {
|
||||
'merge fields were invalid': 'user has never subscribed',
|
||||
'could not be validated':
|
||||
'user has previously unsubscribed or new email already exist on list',
|
||||
'is already a list member': 'new email is already on mailing list',
|
||||
'looks fake or invalid': 'mail looks fake to mailchimp',
|
||||
}
|
||||
|
||||
try {
|
||||
const path = getSubscriberPath(oldEmail)
|
||||
await mailchimp.patch(path, {
|
||||
email_address: newEmail,
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
logger.debug(
|
||||
{ newEmail, listName },
|
||||
'finished changing email in the newsletter'
|
||||
)
|
||||
} catch (err) {
|
||||
// silently ignore users who were never subscribed
|
||||
if (err.status === 404) {
|
||||
return
|
||||
}
|
||||
|
||||
// look through expected mailchimp errors and log if we find one
|
||||
Object.keys(errors).forEach(key => {
|
||||
if (err.message.includes(key)) {
|
||||
const message = `unable to change email in newsletter, ${errors[key]}`
|
||||
|
||||
logger.debug({ oldEmail, newEmail, listName }, message)
|
||||
|
||||
throw new NonFatalEmailUpdateError(
|
||||
message,
|
||||
oldEmail,
|
||||
newEmail
|
||||
).withCause(err)
|
||||
}
|
||||
})
|
||||
|
||||
// if we didn't find an expected error, generate something to throw
|
||||
throw OError.tag(err, 'error changing email in newsletter', {
|
||||
oldEmail,
|
||||
newEmail,
|
||||
listName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscriberPath(email) {
|
||||
const emailHash = hashEmail(email)
|
||||
return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}`
|
||||
}
|
||||
|
||||
function hashEmail(email) {
|
||||
return crypto.createHash('md5').update(email.toLowerCase()).digest('hex')
|
||||
}
|
||||
|
||||
function getMergeFields(user) {
|
||||
return {
|
||||
FNAME: user.first_name,
|
||||
LNAME: user.last_name,
|
||||
MONGO_ID: user._id.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeNullProvider(listName) {
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
logger.debug(
|
||||
{ user, listName },
|
||||
'Not checking user because no newsletter provider is configured'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
logger.debug(
|
||||
{ user, listName },
|
||||
'Not subscribing user to newsletter because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
|
||||
async function unsubscribe(user) {
|
||||
logger.debug(
|
||||
{ user, listName },
|
||||
'Not unsubscribing user from newsletter because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
|
||||
async function changeEmail(user, newEmail) {
|
||||
logger.debug(
|
||||
{ userId: user._id, newEmail, listName },
|
||||
'Not changing email in newsletter for user because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,248 +1,9 @@
|
|||
const { callbackify } = require('util')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const crypto = require('crypto')
|
||||
const Mailchimp = require('mailchimp-api-v3')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const MailchimpProvider = require('./MailChimpProvider')
|
||||
|
||||
const provider = getProvider()
|
||||
const provider = MailchimpProvider.make(
|
||||
'newsletter',
|
||||
Settings.mailchimp ? Settings.mailchimp.list_id : null
|
||||
)
|
||||
|
||||
module.exports = {
|
||||
subscribed: callbackify(provider.subscribed),
|
||||
subscribe: callbackify(provider.subscribe),
|
||||
unsubscribe: callbackify(provider.unsubscribe),
|
||||
changeEmail: callbackify(provider.changeEmail),
|
||||
promises: provider,
|
||||
}
|
||||
|
||||
class NonFatalEmailUpdateError extends OError {
|
||||
constructor(message, oldEmail, newEmail) {
|
||||
super(message, { oldEmail, newEmail })
|
||||
}
|
||||
}
|
||||
|
||||
function getProvider() {
|
||||
if (mailchimpIsConfigured()) {
|
||||
logger.debug('Using newsletter provider: mailchimp')
|
||||
return makeMailchimpProvider()
|
||||
} else {
|
||||
logger.debug('Using newsletter provider: none')
|
||||
return makeNullProvider()
|
||||
}
|
||||
}
|
||||
|
||||
function mailchimpIsConfigured() {
|
||||
return Settings.mailchimp != null && Settings.mailchimp.api_key != null
|
||||
}
|
||||
|
||||
function makeMailchimpProvider() {
|
||||
const mailchimp = new Mailchimp(Settings.mailchimp.api_key)
|
||||
const MAILCHIMP_LIST_ID = Settings.mailchimp.list_id
|
||||
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
const result = await mailchimp.get(path)
|
||||
return result?.status === 'subscribed'
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return false
|
||||
}
|
||||
throw OError.tag(err, 'error getting newsletter subscriptions status', {
|
||||
userId: user._id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
await mailchimp.put(path, {
|
||||
email_address: user.email,
|
||||
status: 'subscribed',
|
||||
status_if_new: 'subscribed',
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
logger.debug({ user }, 'finished subscribing user to newsletter')
|
||||
} catch (err) {
|
||||
throw OError.tag(err, 'error subscribing user to newsletter', {
|
||||
userId: user._id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribe(user, options = {}) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
if (options.delete) {
|
||||
await mailchimp.delete(path)
|
||||
} else {
|
||||
await mailchimp.patch(path, {
|
||||
status: 'unsubscribed',
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
}
|
||||
logger.debug(
|
||||
{ user, options },
|
||||
'finished unsubscribing user from newsletter'
|
||||
)
|
||||
} catch (err) {
|
||||
if (err.status === 404 || err.status === 405) {
|
||||
// silently ignore users who were never subscribed (404) or previously deleted (405)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.message.includes('looks fake or invalid')) {
|
||||
logger.debug(
|
||||
{ err, user, options },
|
||||
'Mailchimp declined to unsubscribe user because it finds the email looks fake'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
throw OError.tag(err, 'error unsubscribing user from newsletter', {
|
||||
userId: user._id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function changeEmail(user, newEmail) {
|
||||
const oldEmail = user.email
|
||||
|
||||
try {
|
||||
await updateEmailInMailchimp(user, newEmail)
|
||||
} catch (updateError) {
|
||||
// if we failed to update the user, delete their old email address so that
|
||||
// we don't leave it stuck in mailchimp
|
||||
logger.debug(
|
||||
{ oldEmail, newEmail, updateError },
|
||||
'unable to change email in newsletter, removing old mail'
|
||||
)
|
||||
|
||||
try {
|
||||
await unsubscribe(user, { delete: true })
|
||||
} catch (unsubscribeError) {
|
||||
// something went wrong removing the user's address
|
||||
throw OError.tag(
|
||||
unsubscribeError,
|
||||
'error unsubscribing old email in response to email change failure',
|
||||
{ oldEmail, newEmail, updateError }
|
||||
)
|
||||
}
|
||||
|
||||
if (!(updateError instanceof NonFatalEmailUpdateError)) {
|
||||
throw updateError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEmailInMailchimp(user, newEmail) {
|
||||
const oldEmail = user.email
|
||||
|
||||
// mailchimp doesn't give us error codes, so we have to parse the message :'(
|
||||
const errors = {
|
||||
'merge fields were invalid': 'user has never subscribed',
|
||||
'could not be validated':
|
||||
'user has previously unsubscribed or new email already exist on list',
|
||||
'is already a list member': 'new email is already on mailing list',
|
||||
'looks fake or invalid': 'mail looks fake to mailchimp',
|
||||
}
|
||||
|
||||
try {
|
||||
const path = getSubscriberPath(oldEmail)
|
||||
await mailchimp.patch(path, {
|
||||
email_address: newEmail,
|
||||
merge_fields: getMergeFields(user),
|
||||
})
|
||||
logger.debug('finished changing email in the newsletter')
|
||||
} catch (err) {
|
||||
// silently ignore users who were never subscribed
|
||||
if (err.status === 404) {
|
||||
return
|
||||
}
|
||||
|
||||
// look through expected mailchimp errors and log if we find one
|
||||
Object.keys(errors).forEach(key => {
|
||||
if (err.message.includes(key)) {
|
||||
const message = `unable to change email in newsletter, ${errors[key]}`
|
||||
|
||||
logger.debug({ oldEmail, newEmail }, message)
|
||||
|
||||
throw new NonFatalEmailUpdateError(
|
||||
message,
|
||||
oldEmail,
|
||||
newEmail
|
||||
).withCause(err)
|
||||
}
|
||||
})
|
||||
|
||||
// if we didn't find an expected error, generate something to throw
|
||||
throw OError.tag(err, 'error changing email in newsletter', {
|
||||
oldEmail,
|
||||
newEmail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscriberPath(email) {
|
||||
const emailHash = hashEmail(email)
|
||||
return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}`
|
||||
}
|
||||
|
||||
function hashEmail(email) {
|
||||
return crypto.createHash('md5').update(email.toLowerCase()).digest('hex')
|
||||
}
|
||||
|
||||
function getMergeFields(user) {
|
||||
return {
|
||||
FNAME: user.first_name,
|
||||
LNAME: user.last_name,
|
||||
MONGO_ID: user._id.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeNullProvider() {
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
logger.debug(
|
||||
{ user },
|
||||
'Not checking user because no newsletter provider is configured'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
logger.debug(
|
||||
{ user },
|
||||
'Not subscribing user to newsletter because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
|
||||
async function unsubscribe(user) {
|
||||
logger.debug(
|
||||
{ user },
|
||||
'Not unsubscribing user from newsletter because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
|
||||
async function changeEmail(oldEmail, newEmail) {
|
||||
logger.debug(
|
||||
{ oldEmail, newEmail },
|
||||
'Not changing email in newsletter for user because no newsletter provider is configured'
|
||||
)
|
||||
}
|
||||
}
|
||||
module.exports = provider
|
||||
|
|
|
@ -765,7 +765,7 @@ const ProjectController = {
|
|||
)
|
||||
User.findById(
|
||||
userId,
|
||||
'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace',
|
||||
'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram labsProgramGalileo',
|
||||
(err, user) => {
|
||||
// Handle case of deleted user
|
||||
if (user == null) {
|
||||
|
@ -1197,6 +1197,8 @@ const ProjectController = {
|
|||
refProviders: _.mapValues(user.refProviders, Boolean),
|
||||
alphaProgram: user.alphaProgram,
|
||||
betaProgram: user.betaProgram,
|
||||
labsProgram: user.labsProgram,
|
||||
labsProgramGalileo: user.labsProgramGalileo,
|
||||
isAdmin: hasAdminAccess(user),
|
||||
},
|
||||
userSettings: {
|
||||
|
|
|
@ -290,8 +290,16 @@ const UserController = {
|
|||
OError.tag(err, 'error unsubscribing to newsletter')
|
||||
return next(err)
|
||||
}
|
||||
return res.json({
|
||||
message: req.i18n.translate('thanks_settings_updated'),
|
||||
// TODO: figure out why things go wrong if we import at the top
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
Modules.hooks.fire('newsletterUnsubscribed', user, err => {
|
||||
if (err) {
|
||||
OError.tag(err, 'error firing "newsletterUnsubscribed" hook')
|
||||
return next(err)
|
||||
}
|
||||
return res.json({
|
||||
message: req.i18n.translate('thanks_settings_updated'),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -73,6 +73,7 @@ async function settingsPage(req, res) {
|
|||
last_name: user.last_name,
|
||||
alphaProgram: user.alphaProgram,
|
||||
betaProgram: user.betaProgram,
|
||||
labsProgram: user.labsProgram,
|
||||
features: {
|
||||
dropbox: user.features.dropbox,
|
||||
github: user.features.github,
|
||||
|
|
|
@ -217,6 +217,16 @@ async function setDefaultEmailAddress(
|
|||
'Failed to change email in newsletter subscription'
|
||||
)
|
||||
}
|
||||
try {
|
||||
// TODO: figure out why things go wrong if we import at the top
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
await Modules.promises.hooks.fire('userEmailChanged', user, email)
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, oldEmail, newEmail: email },
|
||||
'Failed to fire "userEmailChanged" hook'
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await RecurlyWrapper.promises.updateAccountEmailAddress(user._id, email)
|
||||
|
|
|
@ -411,6 +411,7 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
|
|||
Settings.analytics.ga.tokenV4,
|
||||
cookieDomain: Settings.cookieDomain,
|
||||
templateLinks: Settings.templateLinks,
|
||||
labsEnabled: Settings.labs && Settings.labs.enable,
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
|
|
@ -21,7 +21,7 @@ function loadModules() {
|
|||
require(settingsCheckModule)
|
||||
}
|
||||
|
||||
for (const moduleName of Settings.moduleImportSequence) {
|
||||
for (const moduleName of Settings.moduleImportSequence || []) {
|
||||
const loadedModule = require(Path.join(
|
||||
MODULE_BASE_PATH,
|
||||
moduleName,
|
||||
|
|
|
@ -163,6 +163,8 @@ const UserSchema = new Schema({
|
|||
},
|
||||
alphaProgram: { type: Boolean, default: false }, // experimental features
|
||||
betaProgram: { type: Boolean, default: false },
|
||||
labsProgram: { type: Boolean, default: false },
|
||||
labsProgramGalileo: { type: Boolean, default: false },
|
||||
overleaf: {
|
||||
id: { type: Number },
|
||||
accessToken: { type: String },
|
||||
|
|
|
@ -409,6 +409,11 @@
|
|||
"or": "",
|
||||
"other_logs_and_files": "",
|
||||
"other_output_files": "",
|
||||
"overleaf_labs": "",
|
||||
"labs_program_benefits": "",
|
||||
"labs_program_already_participating": "",
|
||||
"labs_program_not_participating": "",
|
||||
"manage_labs_program_membership": "",
|
||||
"owner": "",
|
||||
"page_current": "",
|
||||
"pagination_navigation": "",
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
|
||||
function LabsProgramSection() {
|
||||
const { t } = useTranslation()
|
||||
const { labsProgram } = useUserContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>{t('overleaf_labs')}</h3>
|
||||
{labsProgram ? null : (
|
||||
<p className="small">
|
||||
<Trans i18nKey="labs_program_benefits">
|
||||
<span />
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
<p className="small">
|
||||
{labsProgram
|
||||
? t('labs_program_already_participating')
|
||||
: t('labs_program_not_participating')}
|
||||
</p>
|
||||
<a href="/labs/participate">{t('manage_labs_program_membership')}</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabsProgramSection
|
|
@ -6,6 +6,7 @@ import AccountInfoSection from './account-info-section'
|
|||
import PasswordSection from './password-section'
|
||||
import LinkingSection from './linking-section'
|
||||
import BetaProgramSection from './beta-program-section'
|
||||
import LabsProgramSection from './labs-program-section'
|
||||
import SessionsSection from './sessions-section'
|
||||
import NewsletterSection from './newsletter-section'
|
||||
import LeaveSection from './leave-section'
|
||||
|
@ -38,7 +39,9 @@ function SettingsPageRoot() {
|
|||
|
||||
function SettingsPageContent() {
|
||||
const { t } = useTranslation()
|
||||
const { isOverleaf } = getMeta('ol-ExposedSettings') as ExposedSettings
|
||||
const { isOverleaf, labsEnabled } = getMeta(
|
||||
'ol-ExposedSettings'
|
||||
) as ExposedSettings
|
||||
|
||||
return (
|
||||
<UserProvider>
|
||||
|
@ -67,6 +70,12 @@ function SettingsPageContent() {
|
|||
<hr />
|
||||
</>
|
||||
) : null}
|
||||
{labsEnabled ? (
|
||||
<>
|
||||
<LabsProgramSection />
|
||||
<hr />
|
||||
</>
|
||||
) : null}
|
||||
<SessionsSection />
|
||||
{isOverleaf ? (
|
||||
<>
|
||||
|
|
|
@ -15,6 +15,7 @@ UserContext.Provider.propTypes = {
|
|||
last_name: PropTypes.string,
|
||||
alphaProgram: PropTypes.boolean,
|
||||
betaProgram: PropTypes.boolean,
|
||||
labsProgram: PropTypes.boolean,
|
||||
features: PropTypes.shape({
|
||||
dropbox: PropTypes.boolean,
|
||||
github: PropTypes.boolean,
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
content: 'β';
|
||||
}
|
||||
}
|
||||
|
||||
.alpha-badge {
|
||||
background-color: @ol-green;
|
||||
border-radius: @border-radius-base;
|
||||
|
@ -56,6 +55,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.labs-badge {
|
||||
background-color: @orange;
|
||||
border-radius: @border-radius-base;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.split-test-badge-tooltip .tooltip-inner {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -1876,5 +1876,20 @@
|
|||
"reverse_x_sort_order" : "Reverse __x__ sort order",
|
||||
"create_first_project": "Create First Project",
|
||||
"you_dont_have_any_repositories": "You don’t have any repositories",
|
||||
"tag_name_cannot_exceed_characters": "Tag name cannot exceed __maxLength__ characters"
|
||||
"tag_name_cannot_exceed_characters": "Tag name cannot exceed __maxLength__ characters",
|
||||
"overleaf_labs": "Overleaf Labs",
|
||||
"labs_program_already_participating": "You are enrolled in Labs",
|
||||
"labs_program_not_participating": "You are not enrolled in Labs",
|
||||
"labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.",
|
||||
"you_can_opt_in_and_out_of_overleaf_labs_at_any_time_on_this_page": "You can <0>opt in and out</0> of Overleaf Labs at any time on this page",
|
||||
"you_can_opt_in_and_out_of_galileo_at_any_time_on_this_page": "You can <0>opt in and out</0> of Galileo at any time on this page",
|
||||
"you_can_opt_in_to_individual_experiments": "You will be asked to opt in and out of individual experiments; each experiment may have unique partners, requirements, terms and conditions, etc. that must be opted in to for that specific experiment",
|
||||
"labs_program_badge_description": "While using __appName__, you will see Labs features marked with this badge:",
|
||||
"note_experiments_under_development": "<0>Please note</0> that experiments in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a paid plan</0>",
|
||||
"galileo_program_description": "Galileo is an AI that helps you write your documents",
|
||||
"galileo_is_part_of_overleaf_labs": "Galileo is an experiment in <0>Overleaf Labs</0>",
|
||||
"thank_you_for_being_part_of_our_labs_program": "Thank you for being part of our Labs program, where you can have <0>early access to experimental features</0> and help us explore innovative ideas that help you work more quickly and effectively",
|
||||
"manage_labs_program_membership": "Manage Labs Program Membership",
|
||||
"current_experiments": "Current Experiments",
|
||||
"overleaf_labs": "Overleaf Labs"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
|
||||
const Helpers = require('./lib/helpers')
|
||||
|
||||
exports.tags = ['saas']
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
key: { labsProgram: 1 },
|
||||
name: 'labsProgram_1',
|
||||
},
|
||||
{
|
||||
key: { labsProgramGalileo: 1 },
|
||||
name: 'labsProgramGalileo_1',
|
||||
},
|
||||
]
|
||||
|
||||
exports.migrate = async client => {
|
||||
const { db } = client
|
||||
await Helpers.addIndexesToCollection(db.users, indexes)
|
||||
}
|
||||
|
||||
exports.rollback = async client => {
|
||||
const { db } = client
|
||||
await Helpers.dropIndexesFromCollection(db.users, indexes)
|
||||
}
|
BIN
services/web/public/img/other-brands/logo_pwc.png
Normal file
BIN
services/web/public/img/other-brands/logo_pwc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -36,4 +36,5 @@ export type ExposedSettings = {
|
|||
textExtensions: string[]
|
||||
validRootDocExtensions: string[]
|
||||
templateLinks: TemplateLink[]
|
||||
labsEnabled: boolean
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue