Bug 1919657 - Account Hub Auto Discovery Options. r=aleca,freaktechnik

**How To Test**
- Open account hub from the developer console (openAccountHub(); then enter)
- Fill name and you can use either an existing email or the test exchange email testaccounts@o365.thunderbird.net
- You can close the auth modal
- Flip through the different config options to ensure the selected config data is right
- Check against the dark mode colors as well

Differential Revision: https://phabricator.services.mozilla.com/D226576

--HG--
extra : amend_source : c4f4367c00638bd77562bed4efdc4666728bb08b
This commit is contained in:
Vineet Deo 2024-11-02 12:48:18 +02:00
Родитель 9f980f4bff
Коммит f37661f1a0
13 изменённых файлов: 1170 добавлений и 57 удалений

Просмотреть файл

@ -45,6 +45,10 @@ class AccountHubFooter extends HTMLElement {
this.querySelector("#forward").hidden = !val; this.querySelector("#forward").hidden = !val;
} }
toggleForwardDisabled(val) {
this.querySelector("#forward").disabled = val;
}
canCustom(val) { canCustom(val) {
const customAction = this.querySelector("#custom"); const customAction = this.querySelector("#custom");
customAction.hidden = !val; customAction.hidden = !val;

Просмотреть файл

@ -72,6 +72,11 @@ class EmailAutoForm extends AccountHubStep {
}); });
} }
resetState() {
this.querySelector("#autoConfigEmailForm").reset();
this.#currentConfig = {};
}
/** /**
* Check whether the user entered the minimum amount of information needed to * Check whether the user entered the minimum amount of information needed to
* update the hostname and domain for the complete form. * update the hostname and domain for the complete form.
@ -79,6 +84,14 @@ class EmailAutoForm extends AccountHubStep {
#checkValidEmailForm() { #checkValidEmailForm() {
const isValidForm = const isValidForm =
this.#email.checkValidity() && this.#realName.checkValidity(); this.#email.checkValidity() && this.#realName.checkValidity();
this.dispatchEvent(
new CustomEvent("config-updated", {
bubbles: true,
detail: { completed: isValidForm },
})
);
const domain = isValidForm const domain = isValidForm
? this.#email.value.split("@")[1].toLowerCase() ? this.#email.value.split("@")[1].toLowerCase()
: ""; : "";

Просмотреть файл

@ -4,11 +4,30 @@
import { AccountHubStep } from "./account-hub-step.mjs"; import { AccountHubStep } from "./account-hub-step.mjs";
const { Sanitizer } = ChromeUtils.importESModule(
"resource:///modules/accountcreation/Sanitizer.sys.mjs"
);
/** /**
* Account Hub Config Found Template * Account Hub Config Found Template
* Template ID: #accountHubConfigFoundTemplate (from accountHubConfigFoundTemplate.inc.xhtml) * Template ID: #accountHubConfigFoundTemplate (from accountHubConfigFoundTemplate.inc.xhtml)
*/ */
class EmailConfigFound extends AccountHubStep { class EmailConfigFound extends AccountHubStep {
/**
* The current email auto config form inputs.
*
* @type {AccountConfig}
*/
#currentConfig;
/**
* The email auto config form.
*
* @type {HTMLElement}
*/
#protocolForm;
connectedCallback() { connectedCallback() {
if (this.hasConnected) { if (this.hasConnected) {
super.connectedCallback(); super.connectedCallback();
@ -22,6 +41,148 @@ class EmailConfigFound extends AccountHubStep {
.getElementById("accountHubEmailConfigFoundTemplate") .getElementById("accountHubEmailConfigFoundTemplate")
.content.cloneNode(true); .content.cloneNode(true);
this.appendChild(template); this.appendChild(template);
this.#protocolForm = this.querySelector("#protocolForm");
this.#protocolForm.addEventListener("change", event => {
// Remove 'selected' class from all label elements.
this.querySelectorAll("label.selected").forEach(label => {
label.classList.remove("selected");
});
// Add 'selected' class to the parent label of the selected radio button.
event.target.closest("label").classList.add("selected");
this.#selectConfig(event.target.value);
});
this.#currentConfig = {};
}
/**
* Return the current state of the email setup form.
*/
captureState() {
return this.#currentConfig;
}
/**
* Sets the state of the email config found state.
*
* @param {AccountConfig} configData - Applies the config data to this state.
*/
setState(configData) {
this.#currentConfig = configData;
this.#updateFields();
}
/**
* Updates the select config options.
*/
#updateFields() {
if (!this.#currentConfig) {
return;
}
const configLabels = [
this.querySelector("#imap"),
this.querySelector("#pop3"),
this.querySelector("#exchange"),
];
const alternatives = this.#currentConfig.incomingAlternatives.map(
a => a.type
);
// Initially hide all config options and reset recommended class.
for (const config of configLabels) {
config.hidden =
config.id !== this.#currentConfig.incoming.type &&
!alternatives.includes(config.id);
config.classList.toggle(
"recommended-protocol",
config.id === this.#currentConfig.incoming.type
);
config.querySelector("input").checked =
config.id === this.#currentConfig.incoming.type;
}
// Dispatch a change event so config selection logic can run.
const recommendedTypeLabel = this.querySelector(
`#${this.#currentConfig.incoming.type}`
);
const event = new Event("change", { bubbles: true });
recommendedTypeLabel.querySelector("input").dispatchEvent(event);
}
/**
* Sets the current selected config.
*
* @param {String} configType - The config type (imap, pop3, exchange).
*/
#selectConfig(configType) {
const username = this.#currentConfig.incoming.username;
// Grab the config from the list of configs in #currentConfig.
const incoming = [
this.#currentConfig.incoming,
...this.#currentConfig.incomingAlternatives,
].find(({ type }) => type === configType);
const outgoing = this.#currentConfig.outgoing;
this.querySelector("#incomingType").textContent = incoming.type;
this.querySelector("#incomingHost").textContent = incoming.hostname;
this.querySelector("#incomingUsername").textContent = username;
this.querySelector("#incomingType").title = incoming.type;
this.querySelector("#incomingHost").title = incoming.hostname;
this.querySelector("#incomingUsername").title = username;
const incomingSSL = Sanitizer.translate(incoming.socketType, {
0: "no-encryption",
2: "starttls",
3: "ssl",
});
document.l10n.setAttributes(
this.querySelector("#incomingAuth"),
`account-setup-result-${incomingSSL}`
);
// Hide outgoing config details if unavailable.
if (!outgoing || incoming.type === "exchange") {
this.querySelector("#outgoingConfigType").hidden = true;
this.querySelector("#outgoingConfig").hidden = true;
this.querySelector("#configSelection").classList.add("single");
document.l10n.setAttributes(
this.querySelector("#incomingTypeText"),
"account-hub-result-ews-text"
);
return;
}
this.querySelector("#configSelection").classList.remove("single");
document.l10n.setAttributes(
this.querySelector("#incomingTypeText"),
"account-hub-result-incoming-server-legend"
);
this.querySelector("#outgoingConfigType").hidden = false;
this.querySelector("#outgoingConfig").hidden = false;
this.querySelector("#outgoingType").textContent = outgoing.type;
this.querySelector("#outgoingHost").textContent = outgoing.hostname;
this.querySelector("#outgoingUsername").textContent = outgoing.username;
this.querySelector("#outgoingType").title = outgoing.type;
this.querySelector("#outgoingHost").title = outgoing.hostname;
this.querySelector("#outgoingUsername").title = outgoing.username;
const outgoingSsl = Sanitizer.translate(outgoing.socketType, {
0: "no-encryption",
2: "starttls",
3: "ssl",
});
document.l10n.setAttributes(
this.querySelector("#outgoingAuth"),
`account-setup-result-${outgoingSsl}`
);
} }
} }

Просмотреть файл

@ -3,5 +3,116 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
<html:template id="accountHubEmailConfigFoundTemplate" xmlns="http://www.w3.org/1999/xhtml"> <html:template id="accountHubEmailConfigFoundTemplate" xmlns="http://www.w3.org/1999/xhtml">
<div id="configFound" slot="content"></div> <div id="configFound" slot="content">
<form id="protocolForm" class="account-hub-form">
<div class="hub-body auto-config-body">
<div class="config-form-data">
<fieldset class="form-options">
<label id="imap" class="config-option">
<div class="config-title">
<input type="radio" name="config-type" value="imap" />
IMAP
<span class="recommended" data-l10n-id="account-hub-result-recommended-label"></span>
</div>
<div class="config-text" data-l10n-id="account-hub-result-imap-description"></div>
</label>
<label id="pop3" class="config-option">
<div class="config-title">
<input type="radio" name="config-type" value="pop3" />
POP3
<span class="recommended" data-l10n-id="account-hub-result-recommended-label"></span>
</div>
<div class="config-text" data-l10n-id="account-hub-result-pop-description"></div>
</label>
<label id="exchange" class="config-option">
<div class="config-title">
<input type="radio" name="config-type" value="exchange" />
<span data-l10n-id="account-hub-result-ews-shortname"></span>
<span class="recommended" data-l10n-id="account-hub-result-recommended-label"></span>
</div>
<div class="config-text" data-l10n-id="account-hub-result-ews-description"></div>
</label>
</fieldset>
<div id="configSelection" class="form-selection">
<div class="config-directions selection-grid">
<div class="config-detail" id="incomingConfigType">
<img src="chrome://messenger/skin/icons/new/normal/incoming.svg" alt="" />
<div class="config-item-data">
<p id="incomingType"></p>
<p id="incomingTypeText" data-l10n-id="account-hub-result-incoming-server-legend"></p>
</div>
</div>
<div class="config-detail" id="outgoingConfigType" hidden="hidden">
<img src="chrome://messenger/skin/icons/new/normal/outgoing.svg" alt="" />
<div class="config-item-data">
<p id="outgoingType"></p>
<p data-l10n-id="account-hub-result-outgoing-server-legend"></p>
</div>
</div>
</div>
<div class="selection-grid">
<div class="config-column" id="incomingConfig">
<!-- Host -->
<div class="config-detail">
<img class="config-item-icon lighter" src="chrome://messenger/skin/icons/new/compact/globe.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-hostname-label"></p>
<p id="incomingHost"></p>
</div>
</div>
<!-- Authentication -->
<div class="config-detail">
<img class="config-item-icon" src="chrome://messenger/skin/icons/new/compact/key.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-authentication-label"></p>
<p id="incomingAuth"></p>
</div>
</div>
<!-- Username -->
<div class="config-detail">
<img class="config-item-icon" src="chrome://messenger/skin/icons/new/compact/account-settings.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-username-label"></p>
<p id="incomingUsername"></p>
</div>
</div>
</div>
<div class="config-column" id="outgoingConfig" hidden="hidden">
<!-- Host -->
<div class="config-detail">
<img class="config-item-icon" src="chrome://messenger/skin/icons/new/compact/globe.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-hostname-label"></p>
<p id="outgoingHost"></p>
</div>
</div>
<!-- Authentication -->
<div class="config-detail">
<img class="config-item-icon" src="chrome://messenger/skin/icons/new/compact/key.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-authentication-label"></p>
<p id="outgoingAuth"></p>
</div>
</div>
<!-- Username -->
<div class="config-detail">
<img class="config-item-icon" src="chrome://messenger/skin/icons/new/compact/account-settings.svg" />
<div class="config-item-data">
<p data-l10n-id="account-hub-result-username-label"></p>
<p id="outgoingUsername"></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</html:template> </html:template>

Просмотреть файл

@ -23,7 +23,8 @@
</button> </button>
<button id="forward" <button id="forward"
data-l10n-id="account-hub-email-continue-button" data-l10n-id="account-hub-email-continue-button"
class="footer-primary footer-button"> class="footer-primary footer-button"
disabled="disabled">
</button> </button>
</li> </li>
</menu> </menu>

Просмотреть файл

@ -29,12 +29,8 @@ const { FindConfig } = ChromeUtils.importESModule(
"resource:///modules/accountcreation/FindConfig.sys.mjs" "resource:///modules/accountcreation/FindConfig.sys.mjs"
); );
const { const { gAccountSetupLogger, SuccessiveAbortable, UserCancelledException } =
CancelledException, AccountCreationUtils;
gAccountSetupLogger,
SuccessiveAbortable,
UserCancelledException,
} = AccountCreationUtils;
import "chrome://messenger/content/accountcreation/content/widgets/account-hub-step.mjs"; // eslint-disable-line import/no-unassigned-import import "chrome://messenger/content/accountcreation/content/widgets/account-hub-step.mjs"; // eslint-disable-line import/no-unassigned-import
import "chrome://messenger/content/accountcreation/content/widgets/account-hub-footer.mjs"; // eslint-disable-line import/no-unassigned-import import "chrome://messenger/content/accountcreation/content/widgets/account-hub-footer.mjs"; // eslint-disable-line import/no-unassigned-import
@ -136,6 +132,20 @@ class AccountHubEmail extends HTMLElement {
*/ */
#hasCancelled; #hasCancelled;
/**
* The email for the current user.
*
* @type {String}
*/
#email;
/**
* The real name for the current user.
*
* @type {String}
*/
#realName;
/** /**
* States of the email setup flow, based on the ID's of the steps in the * States of the email setup flow, based on the ID's of the steps in the
* flow. * flow.
@ -147,6 +157,7 @@ class AccountHubEmail extends HTMLElement {
id: "emailAutoConfigSubview", id: "emailAutoConfigSubview",
nextStep: "emailConfigFoundSubview", nextStep: "emailConfigFoundSubview",
previousStep: "", previousStep: "",
forwardEnabled: false,
customActionFluentID: "", customActionFluentID: "",
subview: {}, subview: {},
templateId: "email-auto-form", templateId: "email-auto-form",
@ -155,6 +166,7 @@ class AccountHubEmail extends HTMLElement {
id: "emailConfigFoundSubview", id: "emailConfigFoundSubview",
nextStep: "emailPasswordSubview", nextStep: "emailPasswordSubview",
previousStep: "autoConfigSubview", previousStep: "autoConfigSubview",
forwardEnabled: true,
customActionFluentID: "", customActionFluentID: "",
subview: {}, subview: {},
templateId: "email-config-found", templateId: "email-config-found",
@ -163,6 +175,7 @@ class AccountHubEmail extends HTMLElement {
id: "emailPasswordSubview", id: "emailPasswordSubview",
nextStep: "emailSyncAccountsSubview", nextStep: "emailSyncAccountsSubview",
previousStep: "emailConfigFoundSubview", previousStep: "emailConfigFoundSubview",
forwardEnabled: false,
customActionFluentID: "", customActionFluentID: "",
subview: {}, subview: {},
templateId: "", templateId: "",
@ -171,6 +184,7 @@ class AccountHubEmail extends HTMLElement {
id: "emailSyncAccountsSubview", id: "emailSyncAccountsSubview",
nextStep: "emailAddedSubview", nextStep: "emailAddedSubview",
previousStep: "", previousStep: "",
forwardEnabled: true,
customActionFluentID: "", customActionFluentID: "",
subview: {}, subview: {},
templateId: "", templateId: "",
@ -179,14 +193,16 @@ class AccountHubEmail extends HTMLElement {
id: "emailIncomingConfigSubview", id: "emailIncomingConfigSubview",
nextStep: "emailOutgoingConfigSubview", nextStep: "emailOutgoingConfigSubview",
previousStep: "", previousStep: "",
forwardEnabled: true,
customActionFluentID: "", customActionFluentID: "",
subview: {}, subview: {},
templateId: "account-hub-email-manual-incoming-form", templateId: "email-manual-incoming-form",
}, },
outgoingConfigSubview: { outgoingConfigSubview: {
id: "emailOutgoingConfigSubview", id: "emailOutgoingConfigSubview",
nextStep: "emailAddedSubview", nextStep: "emailAddedSubview",
previousStep: "emailIncomingConfigSubview", previousStep: "emailIncomingConfigSubview",
forwardEnabled: true,
customActionFluentID: "account-hub-test-configuration", customActionFluentID: "account-hub-test-configuration",
subview: {}, subview: {},
templateId: "email-manual-outgoing-form", templateId: "email-manual-outgoing-form",
@ -195,6 +211,7 @@ class AccountHubEmail extends HTMLElement {
id: "emailAddedSubview", id: "emailAddedSubview",
nextStep: "", nextStep: "",
previousStep: "", previousStep: "",
forwardEnabled: true,
customActionFluentID: "account-hub-add-new-email", customActionFluentID: "account-hub-add-new-email",
subview: {}, subview: {},
templateId: "", templateId: "",
@ -246,9 +263,13 @@ class AccountHubEmail extends HTMLElement {
this.#emailFooter.addEventListener("back", this); this.#emailFooter.addEventListener("back", this);
this.#emailFooter.addEventListener("forward", this); this.#emailFooter.addEventListener("forward", this);
this.#emailFooter.addEventListener("custom", this); this.#emailFooter.addEventListener("custom", this);
this.#emailAutoConfigSubview.addEventListener("config-updated", this);
this.abortable = null; this.abortable = null;
this.#hasCancelled = false; this.#hasCancelled = false;
this.#currentConfig = {};
this.#email = "";
this.#realName = "";
await this.#initUI("autoConfigSubview"); await this.#initUI("autoConfigSubview");
} }
@ -315,6 +336,11 @@ class AccountHubEmail extends HTMLElement {
this.#emailFooter.canBack(stateDetails.previousStep); this.#emailFooter.canBack(stateDetails.previousStep);
this.#emailFooter.canForward(stateDetails.nextStep); this.#emailFooter.canForward(stateDetails.nextStep);
this.#emailFooter.canCustom(stateDetails.customActionFluentID); this.#emailFooter.canCustom(stateDetails.customActionFluentID);
// The footer forward button is disabled by default.
if (stateDetails.forwardEnabled) {
this.#emailFooter.toggleForwardDisabled(false);
}
} }
async handleEvent(event) { async handleEvent(event) {
@ -338,12 +364,12 @@ class AccountHubEmail extends HTMLElement {
this.#hasCancelled = false; this.#hasCancelled = false;
const stateData = stateDetails.subview.captureState(); const stateData = stateDetails.subview.captureState();
await this.#handleForwardAction(this.#currentState, stateData); await this.#handleForwardAction(this.#currentState, stateData);
if (!this.#hasCancelled) { // Apply the new state data to the new state.
this.#initUI(stateDetails.nextStep); this.#states[this.#currentState].subview.setState(
} else { this.#currentConfig
this.#hasCancelled = false; );
}
} catch (error) { } catch (error) {
this.#handleAbortable();
stateDetails.subview.showErrorNotification(error.title, error.text); stateDetails.subview.showErrorNotification(error.title, error.text);
} }
break; break;
@ -354,6 +380,9 @@ class AccountHubEmail extends HTMLElement {
stateDetails.subview.showErrorNotification(error.title, error.text); stateDetails.subview.showErrorNotification(error.title, error.text);
} }
break; break;
case "config-updated":
this.#emailFooter.toggleForwardDisabled(!event.detail.completed);
break;
default: default:
break; break;
} }
@ -387,17 +416,40 @@ class AccountHubEmail extends HTMLElement {
* button is pressed. * button is pressed.
* *
* @param {String} currentState - The current state of the email flow. * @param {String} currentState - The current state of the email flow.
* @param {String} stateData - The current state data of the email flow. * @param {Object} stateData - The current state data of the email flow.
*/ */
async #handleForwardAction(currentState, stateData) { async #handleForwardAction(currentState, stateData) {
switch (currentState) { switch (currentState) {
case "autoConfigSubview": case "autoConfigSubview":
try { try {
this.#emailFooter.canBack(true); this.#emailFooter.canBack(true);
await this.#findConfig(stateData); this.#email = stateData.email;
this.#realName = stateData.realName;
const config = await this.#findConfig();
// The config is null if guessConfig couldn't find anything, or is
// cancelled. At this point we determine if the user actually
// cancelled, move to the manual config form to get them to fill in
// details, or move forward to the next step.
if (!config) {
if (!this.#hasCancelled) {
this.#currentConfig = this.#fillAccountConfig(
new AccountConfig()
);
await this.#initUI("incomingConfigSubview");
} else {
this.#hasCancelled = false;
break;
}
} else {
this.#currentConfig = this.#fillAccountConfig(config);
await this.#initUI(this.#states[this.#currentState].nextStep);
}
} catch (error) { } catch (error) {
this.#emailFooter.canBack(false); this.#emailFooter.canBack(false);
throw error; if (!(error instanceof UserCancelledException)) {
// TODO: Throw proper error here;
throw error;
}
} }
break; break;
case "incomingConfigSubview": case "incomingConfigSubview":
@ -440,35 +492,35 @@ class AccountHubEmail extends HTMLElement {
#handleAbortable() { #handleAbortable() {
if (this.abortable) { if (this.abortable) {
this.abortable.cancel(new UserCancelledException()); this.abortable.cancel(new UserCancelledException());
this.abortable = null;
} }
} }
/** /**
* Finds an account configuration from the provided data if available. * Finds an account configuration from the provided data if available.
* *
* @param {String} configData - The form config data from initial email form.
*/ */
async #findConfig(configData) { async #findConfig() {
if (this.abortable) { if (this.abortable) {
this.#handleAbortable(); this.#handleAbortable();
} }
const accountConfig = new AccountConfig(); const emailSplit = this.#email.split("@");
const emailSplit = configData.email.split("@");
const emailLocal = Sanitizer.nonemptystring(emailSplit[0]);
accountConfig.incoming.username = emailLocal;
accountConfig.outgoing.username = emailLocal;
const domain = emailSplit[1]; const domain = emailSplit[1];
const initialConfig = new AccountConfig();
const emailLocal = Sanitizer.nonemptystring(emailSplit[0]);
initialConfig.incoming.username = emailLocal;
initialConfig.outgoing.username = emailLocal;
gAccountSetupLogger.debug("findConfig()"); gAccountSetupLogger.debug("findConfig()");
this.abortable = new SuccessiveAbortable(); this.abortable = new SuccessiveAbortable();
let config = null; let config = {};
try { try {
config = await FindConfig.parallelAutoDiscovery( config = await FindConfig.parallelAutoDiscovery(
this.abortable, this.abortable,
domain, domain,
configData.email this.#email
); );
} catch (error) { } catch (error) {
// Error would be thrown if autoDiscovery caused a 401 error. // Error would be thrown if autoDiscovery caused a 401 error.
@ -481,10 +533,18 @@ class AccountHubEmail extends HTMLElement {
this.abortable = null; this.abortable = null;
if (config === null) { if (!config) {
// TODO: Run guessConfig on user input, which prompt return error try {
// error notification if nothing found. config = await this.#guessConfig(domain, initialConfig);
} catch (error) {
// We are returning the initial null config here, as guessConfig does
// not discern errors and always moves to manual config if nothing is
// found.
return config;
}
} }
return config;
} }
/** /**
@ -495,35 +555,64 @@ class AccountHubEmail extends HTMLElement {
// Clear error notifications. // Clear error notifications.
this.#clearNotifications(); this.#clearNotifications();
// TODO: Do guess config on manual config data.
}
/**
* Guess an account configuration with the provided domain.
*
* @param {String} domain - The domain from the email address.
* @param {AccountConifg} initialConifg - Account Config object.
*/
#guessConfig(domain, initialConfig) {
let configType = "both";
if (initialConfig.outgoing?.existingServerKey) {
configType = "incoming";
}
const { promise, resolve, reject } = Promise.withResolvers();
this.abortable = GuessConfig.guessConfig( this.abortable = GuessConfig.guessConfig(
this.#currentConfig.domain, domain,
(type, hostname, port) => { (type, hostname, port, socketType) => {
// The guessConfig search progress is ongoing.
gAccountSetupLogger.debug( gAccountSetupLogger.debug(
`progress callback host: ${hostname}, port: ${port}, type: ${type}` `${hostname}:${port} socketType=${socketType} ${type}: progress callback`
); );
}, },
// eslint-disable-next-line no-unused-vars
config => { config => {
// This will validate and fill all of the form fields, as well as // The guessConifg was successful.
// enable the continue button.
this.abortable = null; this.abortable = null;
// TODO: Update form fields for both incoming and outgoing here with resolve(config);
// the config object.
}, },
error => { e => {
gAccountSetupLogger.warn(`guessConfig failed: ${e}`);
reject(e);
this.abortable = null; this.abortable = null;
// guessConfig failed.
if (error instanceof CancelledException) {
return;
}
gAccountSetupLogger.warn(`guessConfig failed: ${error}`);
// Load the manual config view again and show an error notification.
this.showErrorNotification("account-hub-find-settings-failed", "");
}, },
this.#currentConfig, initialConfig,
this.#currentConfig.outgoing.existingServerKey ? "incoming" : "both" configType
); );
return promise;
}
/**
* Adds name and email address to AccountConfig object.
*
* @param {AccountConfig} accountConfig - AccountConfig from findConfig().
* @param {String} password - The password for the account.
* @returns {AccountConfig} - The concrete AccountConfig object.
*/
#fillAccountConfig(configData, password = "") {
AccountConfig.replaceVariables(
configData,
this.#realName,
this.#email,
password
);
return configData;
} }
/** /**
@ -658,7 +747,19 @@ class AccountHubEmail extends HTMLElement {
* @returns {boolean} - If the account hub can remove this view. * @returns {boolean} - If the account hub can remove this view.
*/ */
reset() { reset() {
// Reset saved for when additional accounts can be added in Account Hub. if (this.abortable) {
return false;
}
this.#currentState = "autoConfigSubview";
this.#currentConfig = {};
this.#hideSubviews();
this.#clearNotifications();
this.#states[this.#currentState].subview.hidden = false;
this.#setFooterButtons();
this.#states[this.#currentState].subview.resetState();
this.#emailFooter.toggleForwardDisabled(true);
return true;
} }
} }

Просмотреть файл

@ -69,13 +69,19 @@ account-hub-email-continue-button = Continue
account-hub-email-confirm-button = Confirm account-hub-email-confirm-button = Confirm
account-hub-incoming-server-legend = Incoming server account-hub-result-incoming-server-legend = Incoming server
.title = Incoming server
account-hub-outgoing-server-legend = Outgoing server account-hub-result-outgoing-server-legend = Outgoing server
.title = Outgoing server
account-hub-protocol-label = Protocol account-hub-protocol-label = Protocol
account-hub-hostname-label = Hostname account-hub-result-hostname-label = Hostname
.title = Hostname
account-hub-result-authentication-label = Authentication
.title = Authentication
account-hub-port-label = Port account-hub-port-label = Port
.title = Set the port number to 0 for autodetection .title = Set the port number to 0 for autodetection
@ -108,7 +114,8 @@ account-hub-auth-no-authentication-option =
account-hub-auth-label = Authentication method account-hub-auth-label = Authentication method
account-hub-username-label = Username account-hub-result-username-label = Username
.title = Username
account-hub-name-label = Full name account-hub-name-label = Full name
.accesskey = n .accesskey = n
@ -144,3 +151,15 @@ account-hub-email-sync-accounts = Sync your calendars and address books
account-hub-test-configuration = Test account-hub-test-configuration = Test
account-hub-add-new-email = Add another email account-hub-add-new-email = Add another email
account-hub-result-imap-description = Keep your folders and emails synced on your server
account-hub-result-pop-description = Keep your folders and emails on your computer
account-hub-result-ews-shortname = Exchange
account-hub-result-ews-description = Use Microsoft Exchange Web Services to sync your folders and emails
account-hub-result-ews-text = Server
account-hub-result-recommended-label = Recommended

Просмотреть файл

@ -4,10 +4,89 @@
"use strict"; "use strict";
const { nsMailServer } = ChromeUtils.importESModule(
"resource://testing-common/mailnews/Maild.sys.mjs"
);
const emailUser = {
name: "John Doe",
email: "john.doe@momo.invalid",
password: "abc12345",
incomingHost: "mail.momo.invalid",
outgoingHost: "mail.momo.invalid",
};
const IMAPServer = {
open() {
const {
ImapDaemon,
ImapMessage,
IMAP_RFC2195_extension,
IMAP_RFC3501_handler,
mixinExtension,
} = ChromeUtils.importESModule(
"resource://testing-common/mailnews/Imapd.sys.mjs"
);
IMAPServer.ImapMessage = ImapMessage;
this.daemon = new ImapDaemon();
this.server = new nsMailServer(daemon => {
const handler = new IMAP_RFC3501_handler(daemon);
mixinExtension(handler, IMAP_RFC2195_extension);
handler.kUsername = "john.doe@momo.invalid";
handler.kPassword = "abc12345";
handler.kAuthRequired = true;
handler.kAuthSchemes = ["PLAIN"];
return handler;
}, this.daemon);
this.server.start(1993);
info(`IMAP server started on port ${this.server.port}`);
registerCleanupFunction(() => this.close());
},
close() {
this.server.stop();
},
get port() {
return this.server.port;
},
};
const SMTPServer = {
open() {
const { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.importESModule(
"resource://testing-common/mailnews/Smtpd.sys.mjs"
);
this.daemon = new SmtpDaemon();
this.server = new nsMailServer(daemon => {
const handler = new SMTP_RFC2821_handler(daemon);
handler.kUsername = "john.doe@momo.invalid";
handler.kPassword = "abc12345";
handler.kAuthRequired = true;
handler.kAuthSchemes = ["PLAIN"];
return handler;
}, this.daemon);
this.server.start(1587);
info(`SMTP server started on port ${this.server.port}`);
registerCleanupFunction(() => this.close());
},
close() {
this.server.stop();
},
get port() {
return this.server.port;
},
};
// TODO: Defer this for when the account hub replaces the account setup tab. // TODO: Defer this for when the account hub replaces the account setup tab.
// add_task(async function test_account_hub_opening_at_startup() {}); // add_task(async function test_account_hub_opening_at_startup() {});
add_task(async function test_account_hub_opening() { add_task(async function test_account_hub_opening() {
IMAPServer.open();
SMTPServer.open();
// TODO: Use an actual button once it's implemented in the UI. // TODO: Use an actual button once it's implemented in the UI.
// Open the dialog. // Open the dialog.
await window.openAccountHub(); await window.openAccountHub();
@ -41,3 +120,360 @@ add_task(async function test_account_hub_opening() {
"The dialog element was closed" "The dialog element was closed"
); );
}); });
add_task(async function test_account_email_step() {
// Open the dialog.
await window.openAccountHub();
const hub = document.querySelector("account-hub-container");
await BrowserTestUtils.waitForMutationCondition(
hub,
{ modal: true, subtree: true, childList: true },
() => hub.shadowRoot.querySelector(".account-hub-dialog"),
"The dialog element was created"
);
const dialog = hub.shadowRoot.querySelector(".account-hub-dialog");
const emailFormPrmise = BrowserTestUtils.waitForMutationCondition(
dialog,
{
subtree: true,
childList: true,
},
() => dialog.querySelector("email-auto-form")
);
await emailFormPrmise;
await TestUtils.waitForCondition(
() => BrowserTestUtils.isVisible(dialog.querySelector("email-auto-form")),
"The initial email template is in view."
);
const emailTemplate = dialog.querySelector("email-auto-form");
const nameInput = emailTemplate.querySelector("#realName");
const emailInput = emailTemplate.querySelector("#email");
const footerForward = dialog
.querySelector("#emailFooter")
.querySelector("#forward");
// Ensure fields are empty.
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
nameInput.value = "";
emailInput.value = "";
// Check if the input icons are hidden.
const icons = emailTemplate.querySelectorAll("img");
for (const icon of icons) {
Assert.ok(BrowserTestUtils.isHidden(icon), `${icon.src} is hidden.`);
}
Assert.ok(
footerForward.disabled,
"Account Hub footer forward button is disabled."
);
// Type a full name into the name input element and check for success.
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
let inputEvent = BrowserTestUtils.waitForEvent(
nameInput,
"input",
false,
event => event.target.value === "Test User"
);
EventUtils.sendString("Test User", window);
await inputEvent;
// Move to email input to trigger animation icon.
EventUtils.synthesizeMouseAtCenter(emailInput, {}, window);
const nameSuccessIcon = Array.from(icons).find(img =>
img.classList.contains("icon-success")
);
Assert.ok(
BrowserTestUtils.isVisible(nameSuccessIcon),
"Name success icon is visible."
);
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
// Delete text and move back to name input to reveal error icon.
const clearInputEvent = BrowserTestUtils.waitForEvent(
nameInput,
"input",
false,
event => event.target.value === ""
);
nameInput.value = "a";
EventUtils.synthesizeKey("KEY_Backspace", {}, window);
await clearInputEvent;
Assert.ok(
BrowserTestUtils.isHidden(nameSuccessIcon),
"Name success icon is hidden."
);
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
const nameDangerIcon = Array.from(icons).find(img =>
img.classList.contains("icon-danger")
);
Assert.ok(
BrowserTestUtils.isVisible(nameDangerIcon),
"Name danger icon is visible."
);
// Fill name and incorrect email input, error email icon should be showing.
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
inputEvent = BrowserTestUtils.waitForEvent(
nameInput,
"input",
false,
event => event.target.value === "Test User"
);
EventUtils.sendString("Test User", window);
await inputEvent;
EventUtils.synthesizeMouseAtCenter(emailInput, {}, window);
inputEvent = BrowserTestUtils.waitForEvent(
emailInput,
"input",
false,
event => event.target.value === "testUser@"
);
EventUtils.sendString("testUser@", window);
await inputEvent;
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
Assert.ok(
BrowserTestUtils.isVisible(emailTemplate.querySelector("#emailWarning")),
"Email danger icon is visible."
);
// Fill in correct email input, see email success icon and continue should
// be enabled.
EventUtils.synthesizeMouseAtCenter(emailInput, {}, window);
inputEvent = BrowserTestUtils.waitForEvent(
emailInput,
"input",
false,
event => event.target.value === "testUser@testing.com"
);
EventUtils.sendString("testing.com", window);
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
Assert.ok(
BrowserTestUtils.isHidden(emailTemplate.querySelector("#emailWarning")),
"Email danger icon is hidden."
);
Assert.ok(
BrowserTestUtils.isVisible(emailTemplate.querySelector("#emailSuccess")),
"Email success icon is visible."
);
Assert.ok(!footerForward.disabled, "Continue button is enabled.");
EventUtils.synthesizeMouseAtCenter(
hub.shadowRoot.querySelector("#closeButton"),
{},
window
);
await BrowserTestUtils.waitForEvent(dialog, "close");
IMAPServer.close();
SMTPServer.close();
});
const PREF_NAME = "mailnews.auto_config_url";
const PREF_VALUE = Services.prefs.getCharPref(PREF_NAME);
add_task(async function test_account_email_config_found() {
IMAPServer.open();
SMTPServer.open();
// Set the pref to load a local autoconfig file.
const url =
"http://mochi.test:8888/browser/comm/mail/test/browser/account/xml/";
Services.prefs.setCharPref(PREF_NAME, url);
// Fill in email auto form and click continue, waiting for config found
// view to be shown.
await window.openAccountHub();
const hub = document.querySelector("account-hub-container");
await BrowserTestUtils.waitForMutationCondition(
hub,
{ modal: true, subtree: true, childList: true },
() => hub.shadowRoot.querySelector(".account-hub-dialog"),
"The dialog element was created"
);
const dialog = hub.shadowRoot.querySelector(".account-hub-dialog");
const emailFormPrmise = BrowserTestUtils.waitForMutationCondition(
dialog,
{
subtree: true,
childList: true,
},
() => dialog.querySelector("email-auto-form")
);
await emailFormPrmise;
await TestUtils.waitForCondition(
() => BrowserTestUtils.isVisible(dialog.querySelector("email-auto-form")),
"The initial email template is in view."
);
const emailTemplate = dialog.querySelector("email-auto-form");
const nameInput = emailTemplate.querySelector("#realName");
const emailInput = emailTemplate.querySelector("#email");
const footerForward = dialog
.querySelector("#emailFooter")
.querySelector("#forward");
// Ensure fields are empty.
nameInput.value = "";
emailInput.value = "";
EventUtils.synthesizeMouseAtCenter(nameInput, {}, window);
let inputEvent = BrowserTestUtils.waitForEvent(
nameInput,
"input",
false,
event => event.target.value === emailUser.name
);
EventUtils.sendString(emailUser.name, window);
await inputEvent;
EventUtils.synthesizeMouseAtCenter(emailInput, {}, window);
inputEvent = BrowserTestUtils.waitForEvent(
emailInput,
"input",
false,
event => event.target.value === emailUser.email
);
EventUtils.sendString(emailUser.email, window);
await inputEvent;
Assert.ok(!footerForward.disabled, "Continue button is enabled.");
// Click continue and wait for config found template to be in view.
EventUtils.synthesizeMouseAtCenter(footerForward, {}, window);
const configFoundTemplate = dialog.querySelector("email-config-found");
await TestUtils.waitForCondition(
() => BrowserTestUtils.isVisible(configFoundTemplate),
"The email config found template is in view."
);
await TestUtils.waitForCondition(
() =>
BrowserTestUtils.isVisible(configFoundTemplate.querySelector("#imap")),
"The IMAP config option is visible"
);
Assert.ok(
BrowserTestUtils.isVisible(configFoundTemplate.querySelector("#pop3")),
"POP3 config option is visible"
);
// This config should not include exchange.
Assert.ok(
BrowserTestUtils.isHidden(configFoundTemplate.querySelector("#exchange")),
"Exchange config option is hidden"
);
// POP3 should be the recommended configuration.
Assert.ok(
BrowserTestUtils.isVisible(
configFoundTemplate.querySelector("#pop3").querySelector(".recommended")
),
"POP3 is the recommended config option."
);
// POP3 should be the selected config.
Assert.ok(
configFoundTemplate.querySelector("#pop3").classList.contains("selected"),
"POP3 is the selected config option."
);
// The config details should show the POP3 details.
testConfigResults(configFoundTemplate, "pop");
// Select the IMAP config and check the details match.
EventUtils.synthesizeMouseAtCenter(
configFoundTemplate.querySelector("#imap"),
{},
window
);
Assert.ok(
configFoundTemplate.querySelector("#imap").classList.contains("selected"),
"SMTP is the selected config option."
);
// The config details should show the IMAP details.
testConfigResults(configFoundTemplate, "imap");
EventUtils.synthesizeMouseAtCenter(
hub.shadowRoot.querySelector("#closeButton"),
{},
window
);
await BrowserTestUtils.waitForEvent(dialog, "close");
// // Restore the original pref.
Services.prefs.setCharPref(PREF_NAME, PREF_VALUE);
IMAPServer.close();
SMTPServer.close();
});
function testConfigResults(template, configType) {
const type = configType === "pop" ? "pop3" : configType;
Assert.equal(
template.querySelector("#incomingType").textContent,
type,
`Incoming type is expected type.`
);
Assert.equal(
template.querySelector("#outgoingType").textContent,
"smtp",
`${configType}: Outgoing type is expected type.`
);
Assert.equal(
template.querySelector("#incomingHost").textContent,
`${configType}.mail.momo.invalid`,
`${configType}: Incoming host is ${configType}.mail.momo.invalid.`
);
Assert.equal(
template.querySelector("#outgoingHost").textContent,
"smtp.mail.momo.invalid",
`${configType}: Outgoing host is expected host.`
);
Assert.equal(
template.l10n.getAttributes(template.querySelector("#incomingAuth")).id,
"account-setup-result-ssl",
`${configType}: Incoming auth is expected auth.`
);
Assert.equal(
template.l10n.getAttributes(template.querySelector("#outgoingAuth")).id,
"account-setup-result-ssl",
`${configType}: Outgoing auth is expected auth.`
);
Assert.equal(
template.querySelector("#incomingUsername").textContent,
"john.doe",
`${configType}: Incoming username is expected username.`
);
Assert.equal(
template.querySelector("#outgoingUsername").textContent,
"john.doe",
`${configType}: Outgoing username is expected username.`
);
}

Просмотреть файл

@ -634,12 +634,14 @@
skin/classic/messenger/icons/new/normal/globe-secure.svg (../shared/mail/icons/new/normal/globe-secure.svg) skin/classic/messenger/icons/new/normal/globe-secure.svg (../shared/mail/icons/new/normal/globe-secure.svg)
skin/classic/messenger/icons/new/normal/globe.svg (../shared/mail/icons/new/normal/globe.svg) skin/classic/messenger/icons/new/normal/globe.svg (../shared/mail/icons/new/normal/globe.svg)
skin/classic/messenger/icons/new/normal/inbox.svg (../shared/mail/icons/new/normal/inbox.svg) skin/classic/messenger/icons/new/normal/inbox.svg (../shared/mail/icons/new/normal/inbox.svg)
skin/classic/messenger/icons/new/normal/incoming.svg (../shared/mail/icons/new/normal/incoming.svg)
skin/classic/messenger/icons/new/normal/link.svg (../shared/mail/icons/new/normal/link.svg) skin/classic/messenger/icons/new/normal/link.svg (../shared/mail/icons/new/normal/link.svg)
skin/classic/messenger/icons/new/normal/mail-secure.svg (../shared/mail/icons/new/normal/mail-secure.svg) skin/classic/messenger/icons/new/normal/mail-secure.svg (../shared/mail/icons/new/normal/mail-secure.svg)
skin/classic/messenger/icons/new/normal/mail.svg (../shared/mail/icons/new/normal/mail.svg) skin/classic/messenger/icons/new/normal/mail.svg (../shared/mail/icons/new/normal/mail.svg)
skin/classic/messenger/icons/new/normal/more.svg (../shared/mail/icons/new/normal/more.svg) skin/classic/messenger/icons/new/normal/more.svg (../shared/mail/icons/new/normal/more.svg)
skin/classic/messenger/icons/new/normal/newsletter.svg (../shared/mail/icons/new/normal/newsletter.svg) skin/classic/messenger/icons/new/normal/newsletter.svg (../shared/mail/icons/new/normal/newsletter.svg)
skin/classic/messenger/icons/new/normal/outbox.svg (../shared/mail/icons/new/normal/outbox.svg) skin/classic/messenger/icons/new/normal/outbox.svg (../shared/mail/icons/new/normal/outbox.svg)
skin/classic/messenger/icons/new/normal/outgoing.svg (../shared/mail/icons/new/normal/outgoing.svg)
skin/classic/messenger/icons/new/normal/overflow.svg (../shared/mail/icons/new/normal/overflow.svg) skin/classic/messenger/icons/new/normal/overflow.svg (../shared/mail/icons/new/normal/overflow.svg)
skin/classic/messenger/icons/new/normal/rss.svg (../shared/mail/icons/new/normal/rss.svg) skin/classic/messenger/icons/new/normal/rss.svg (../shared/mail/icons/new/normal/rss.svg)
skin/classic/messenger/icons/new/normal/sent.svg (../shared/mail/icons/new/normal/sent.svg) skin/classic/messenger/icons/new/normal/sent.svg (../shared/mail/icons/new/normal/sent.svg)

Просмотреть файл

@ -263,13 +263,13 @@ header {
margin-inline: 30px; margin-inline: 30px;
max-height: 40vh; max-height: 40vh;
div:last-child {
border-inline-end: none;
}
&.flex-direction-row { &.flex-direction-row {
flex-direction: row; flex-direction: row;
} }
&.auto-config-body {
max-width: 650px;
}
} }
.hub-body-column { .hub-body-column {

Просмотреть файл

@ -337,6 +337,220 @@ div:has(legend) {
} }
} }
.config-form-data {
display: grid;
grid-template-columns: 2fr 60%;
column-gap: 20px;
& .selection-grid {
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: auto;
gap: 12px;
padding-inline: 12px;
& .config-column {
display: flex;
flex-direction: column;
gap: 8px;
}
}
& .form-options {
display: flex;
flex-direction: column;
justify-content: stretch;
margin: 0;
padding: 0;
border: 0;
gap: 10px;
}
& .config-option {
background-color: light-dark(rgba(254, 255, 255, 0.5), rgba(26, 32, 44, 0.5));
display: flex;
flex-direction: column;
border-radius: 10px;
cursor: pointer;
overflow: hidden;
font-size: 1.2rem;
line-height: normal;
border: 1px solid transparent;
& .recommended {
display: none;
text-transform: uppercase;
font-size: 0.9rem;
font-weight: 600;
margin-inline-start: auto;
padding: 2px;
border: 1px solid transparent;
border-radius: 3px;
}
&.recommended-protocol .recommended {
display: flex;
}
& .config-title {
color: light-dark( var(--color-primary-default), #feffff);
display: flex;
align-items: center;
padding-block: 8px 2px;
padding-inline: 8px;
font-weight: bold;
& input[type="radio"] {
appearance: none;
background-color: transparent;
margin-inline-end: 8px;
width: 15px;
height: 15px;
border: 1px solid var(--color-neutral-border-intense);
border-radius: 50%;
display: grid;
place-content: center;
&::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
transform: scale(0);
box-shadow: inset 1em 1em var(--color-neutral-base);
}
&:checked::before {
transform: scale(1);
}
}
}
& .config-text {
color: var(--color-text-base);
font-weight: 400;
padding-block: 2px 6px;
padding-inline: 9px;
font-size: 1.1rem;
}
&.selected {
border-color: var(--color-primary-default);
& .config-title {
background-color: var(--color-primary-default);
color: var(--color-neutral-base);
padding-block: 6px;
}
& .config-text {
color: light-dark(#15427c, var(--color-text-base));
background-color: var(--color-neutral-base);
padding-block: 6px;
}
& .recommended {
background-color: light-dark(var(--color-primary-soft), transparent);
color: light-dark(var(--color-primary-default), currentColor);
}
& input[type="radio"]:checked {
border-width: 2px;
border-color: var(--color-neutral-base);
}
}
}
& .form-selection {
display: flex;
flex-direction: column;
gap: 12px;
background-color: var(--color-neutral-base);
padding: 6px;
border: 1px solid var(--color-primary-default);
border-radius: 10px;
&.single .selection-grid {
grid-template-columns: auto;
}
& .config-item-icon {
height: 20px;
width: 20px;
-moz-context-properties: fill, stroke, stroke-opacity;
fill: light-dark(color-mix(in srgb, #bdbdbd 20%, transparent), color-mix(in srgb, #3d4d67 20%, transparent));
stroke: light-dark(#bdbdbd, #3d4d67);
&.lighter {
fill: light-dark(color-mix(in srgb, #bdbdbd 5%, transparent), color-mix(in srgb, #3d4d67 5%, transparent));
}
}
& .config-directions {
background-color: light-dark(#f0f8ff, #262c40);
padding-block: 10px;
border-radius: 6px;
& .config-detail {
padding-block: 0;
& img {
height: 30px;
width: 30px;
}
& .config-item-data {
p {
&:first-of-type {
color: light-dark(#2176d6, #feffff);
font-weight: 500;
font-size: 1.3rem;
}
&:last-of-type {
font-size: 1rem;
color: var(--color-text-secondary);
}
}
}
}
}
& .config-detail {
display: flex;
flex-direction: row;
align-items: center;
gap: 9px;
& .config-item-data {
overflow: hidden;
display: flex;
flex-direction: column;
gap: 2px;
& p {
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
margin: 0;
&:first-of-type {
font-size: 0.8rem;
text-transform: uppercase;
color: var(--color-text-secondary);
font-weight: 500;
}
&:last-of-type {
color: var(--color-text-base);
font-weight: 400;
}
}
}
}
}
}
.remember-button-container { .remember-button-container {
margin-block-start: -18px; margin-block-start: -18px;

Просмотреть файл

@ -0,0 +1,25 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 0H5.99999C2.68629 0 0 2.6863 0 6V22C0 25.3137 2.68629 28 5.99999 28H22C25.3137 28 28 25.3137 28 22V6C28 2.6863 25.3137 0 22 0Z" fill="#0A84FF"/>
<path d="M22 2H5.99999C3.79086 2 2 3.79086 2 6V22C2 24.2091 3.79086 26 5.99999 26H22C24.2091 26 26 24.2091 26 22V6C26 3.79086 24.2091 2 22 2Z" fill="url(#paint0_linear_368_1361)"/>
<path opacity="0.15" d="M21 4H6.99999C5.34314 4 4 5.34315 4 7V13C4 14.6569 5.34314 16 6.99999 16H21C22.6569 16 24 14.6569 24 13V7C24 5.34315 22.6569 4 21 4Z" fill="black"/>
<path d="M20 6H7.99999C6.89543 6 6 6.89543 6 8V10C6 11.1046 6.89543 12 7.99999 12H20C21.1046 12 22 11.1046 22 10V8C22 6.89543 21.1046 6 20 6Z" fill="#F9F9FA"/>
<path opacity="0.15" d="M23 8H4.99999C3.34314 8 2 9.34315 2 11V19C2 20.6569 3.34314 22 4.99999 22H23C24.6569 22 26 20.6569 26 19V11C26 9.34315 24.6569 8 23 8Z" fill="black"/>
<path d="M6 10C4.892 10 4 10.892 4 12V16C4 17.108 4.892 18 6 18H8.35938C8.77204 19.1672 9.53565 20.1782 10.5455 20.8943C11.5554 21.6104 12.762 21.9966 14 22C15.238 21.9966 16.4446 21.6104 17.4545 20.8943C18.4644 20.1782 19.228 19.1672 19.6406 18H22C23.108 18 24 17.108 24 16V12C24 10.892 23.108 10 22 10H14H6Z" fill="#F9F9FA"/>
<path opacity="0.15" d="M28 15H19C19 17 17 18.9991 14 18.9991C11 18.9991 9 17 9 15C7 15 2.62241 15.016 0 15" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<path d="M0 16V17L-0.0078125 18C-0.0055989 18 -0.00221584 18 0 18V22C0 25.324 2.676 28 6 28H22C25.324 28 28 25.324 28 22V18V16H19C18.7348 16 18.4805 16.1054 18.2929 16.2929C18.1054 16.4805 18 16.7348 18 17C18 18.3333 16.5557 20 14 20C11.4443 20 10 18.3333 10 17C9.99997 16.7348 9.89461 16.4805 9.70708 16.2929C9.51955 16.1054 9.26521 16 9 16H6H0.0078125H0Z" fill="url(#paint1_linear_368_1361)"/>
<path opacity="0.15" d="M28 17H19C19 19 17 20.9991 14 20.9991C11 20.9991 9 19 9 17C7 17 2.62241 17.016 0 17" stroke="white" stroke-linejoin="round"/>
<path opacity="0.15" d="M0 20V22C0 25.324 2.676 28 6 28H22C25.324 28 28 25.324 28 22V20C28 23.324 25.324 26 22 26H6C2.676 26 0 23.324 0 20Z" fill="black"/>
<defs>
<linearGradient id="paint0_linear_368_1361" x1="14" y1="2" x2="14" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#003EAA"/>
<stop offset="1" stop-color="#0A84FF"/>
</linearGradient>
<linearGradient id="paint1_linear_368_1361" x1="14" y1="16" x2="14" y2="28" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A84FF"/>
<stop offset="1" stop-color="#45A1FF"/>
</linearGradient>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.8 KiB

Просмотреть файл

@ -0,0 +1,26 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 0H5.99999C2.68629 0 0 2.6863 0 6V22C0 25.3137 2.68629 28 5.99999 28H22C25.3137 28 28 25.3137 28 22V6C28 2.6863 25.3137 0 22 0Z" fill="#0A84FF"/>
<path d="M22 2H5.99999C3.79086 2 2 3.79086 2 6V22C2 24.2091 3.79086 26 5.99999 26H22C24.2091 26 26 24.2091 26 22V6C26 3.79086 24.2091 2 22 2Z" fill="url(#paint0_linear_368_1373)"/>
<path opacity="0.15" d="M28 15H19C19 17 17 18.9991 14 18.9991C11 18.9991 9 17 9 15C7 15 2.62241 15.016 0 15" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<path d="M0 16V17L-0.0078125 18C-0.0055989 18 -0.00221584 18 0 18V22C0 25.324 2.676 28 6 28H22C25.324 28 28 25.324 28 22V18V16H19C18.7348 16 18.4805 16.1054 18.2929 16.2929C18.1054 16.4805 18 16.7348 18 17C18 18.3333 16.5557 20 14 20C11.4443 20 10 18.3333 10 17C9.99997 16.7348 9.89461 16.4805 9.70708 16.2929C9.51955 16.1054 9.26521 16 9 16H6H0.0078125H0Z" fill="url(#paint1_linear_368_1373)"/>
<path opacity="0.15" d="M28 17H19C19 19 17 20.9991 14 20.9991C11 20.9991 9 19 9 17C7 17 2.62241 17.016 0 17" stroke="white" stroke-linejoin="round"/>
<path opacity="0.15" d="M0 20V22C0 25.324 2.676 28 6 28H22C25.324 28 28 25.324 28 22V20C28 23.324 25.324 26 22 26H6C2.676 26 0 23.324 0 20Z" fill="black"/>
<path opacity="0.15" d="M13.9996 1.99951C13.7025 1.99903 13.4205 2.1307 13.2301 2.35888L8.23004 8.35888C7.68644 9.01074 8.1508 10.0008 8.99957 9.99951H11.9996V14.9996C11.9997 15.5518 12.4474 15.9995 12.9996 15.9996H14.9996C15.5519 15.9995 15.9996 15.5518 15.9996 14.9996V9.99951H18.9996C19.8484 10.0008 20.3128 9.01074 19.7692 8.35888L14.7692 2.35888C14.5788 2.1307 14.2968 1.99903 13.9996 1.99951Z" fill="black"/>
<path opacity="0.15" d="M13.9999 -0.000488062C13.1112 -0.000828062 12.2647 0.395097 11.6952 1.07764L6.69519 7.07764C5.15575 8.92366 6.59727 11.9996 8.99988 11.9996H9.99988V14.9996C10 16.6326 11.3669 17.9994 12.9999 17.9996H14.9999C16.6329 17.9994 17.9997 16.6326 17.9999 14.9996V11.9996H18.9999C21.4025 11.9996 22.844 8.92366 21.3046 7.07764L16.3046 1.07764C15.7351 0.395097 14.8886 -0.000828062 13.9999 -0.000488062Z" fill="black"/>
<path d="M13.9996 2C13.7024 1.99952 13.4204 2.13119 13.23 2.35937L8.23004 8.35937C7.68644 9.01123 8.1508 10.0012 8.99957 10H11.9996V15C11.9996 15.5522 12.4473 15.9999 12.9996 16H14.9996C15.5518 15.9999 15.9995 15.5522 15.9996 15V10H18.9996C19.8483 10.0012 20.3127 9.01123 19.7691 8.35937L14.7691 2.35937C14.5787 2.13119 14.2967 1.99952 13.9996 2Z" fill="#F9F9FA"/>
<path opacity="0.3" d="M8.59723 7.91797L8.23004 8.35938C7.68644 9.01123 8.1508 10.0012 8.99957 10H11.9996V8H8.99957C8.85288 8.00021 8.71774 7.97011 8.59723 7.91797ZM19.4019 7.91797C19.2814 7.97011 19.1463 8.00021 18.9996 8H15.9996V10H18.9996C19.8483 10.0012 20.3127 9.01123 19.7691 8.35938L19.4019 7.91797ZM11.9996 13V15C11.9996 15.5523 12.4473 15.9999 12.9996 16H14.9996C15.5518 15.9999 15.9995 15.5523 15.9996 15V13C15.9995 13.5523 15.5518 13.9999 14.9996 14H12.9996C12.4473 13.9999 11.9996 13.5523 11.9996 13Z" fill="#737373"/>
<defs>
<linearGradient id="paint0_linear_368_1373" x1="14" y1="2" x2="14" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#003EAA"/>
<stop offset="1" stop-color="#0A84FF"/>
</linearGradient>
<linearGradient id="paint1_linear_368_1373" x1="14" y1="16" x2="14" y2="28" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A84FF"/>
<stop offset="1" stop-color="#45A1FF"/>
</linearGradient>
</defs>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB