зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1548404 - Update UITour to reflect the decoupling of FxA and Sync. r=MattN,andreio,rfkelly
Differential Revision: https://phabricator.services.mozilla.com/D51976 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
f9f5d6809a
Коммит
ab41353fa5
|
@ -387,6 +387,8 @@ if (typeof Mozilla == "undefined") {
|
|||
* <li>{@link Mozilla.UITour.Configuration.Search|selectedSearchEngine}
|
||||
* - DEPRECATED, use 'search'</li>
|
||||
* <li>{@link Mozilla.UITour.Configuration.Sync|sync}</li>
|
||||
* - DEPRECATED, use 'fxa'</li>
|
||||
* <li>{@link Mozilla.UITour.Configuration.FxA|fxa}</li>
|
||||
* </ul>
|
||||
*/
|
||||
|
||||
|
@ -448,6 +450,65 @@ if (typeof Mozilla == "undefined") {
|
|||
* @since 50
|
||||
*/
|
||||
|
||||
/**
|
||||
* FxA status, including whether FxA is connected, device counts, services
|
||||
* connected to this browser and services externally connected to the account.
|
||||
* @typedef {Object} Mozilla.UITour.Configuration.FxA
|
||||
* @property {Boolean} setup - Whether FxA is setup on this device. If false,
|
||||
* no other properties will exist.
|
||||
* @property {Number} [numOtherDevices] - Number of devices connected to this
|
||||
* account, not counting this device.
|
||||
* @property {Object.<String, Number>} [numDevicesByType] - A count of devices
|
||||
* connected to the account by device 'type'. Valid values for type are
|
||||
* defined by the FxA server but roughly correspond to form-factor with
|
||||
* values like 'desktop', 'mobile', 'vr', etc.
|
||||
* @property {Mozilla.UITour.Configuration.AccountServices} [accountServices] -
|
||||
* Information about services attached to this account. These services
|
||||
* may be enabled on devices or applications external to this
|
||||
* browser and should not be confused with devices. For example, if the user
|
||||
* has enabled Monitor or Lockwise on one or more devices - including on
|
||||
* this device - that service will have a single entry here.
|
||||
* @property {Mozilla.UITour.Configuration.BrowserServices} [browserServices] -
|
||||
* Information about account services attached to this browser, and with
|
||||
* special support implemented by this browser. You should not expect
|
||||
* every accountService connected in this browser to get a special entry
|
||||
* here. Indeed, what services, and in what circumstances they may appear
|
||||
* here in the future is largely TBD.
|
||||
* @since 71
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about clients attached to the account.
|
||||
* An object. The key is a string ID of the attached service. A list of attached
|
||||
* service IDs can be found at
|
||||
* {@link https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution|
|
||||
* on our telemetry documentation site}
|
||||
* The value is a {@link Mozilla.UITour.Configuration.AccountService}
|
||||
* @typedef {Object.<string, Mozilla.UITour.Configuration.AccountService>} Mozilla.UITour.Configuration.AccountService
|
||||
* @since 71
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about an account service
|
||||
* @typedef {Object} Mozilla.UITour.Configuration.AccountService
|
||||
* @property {String} id - The service ID. A list of attached
|
||||
* service IDs can be found at
|
||||
* {@link https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution|
|
||||
* on our telemetry documentation site}
|
||||
* @property {Number} lastAccessedWeeksAgo - How many weeks ago the service
|
||||
* was accessed by this account.
|
||||
* @since 71
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about a services attached to the browser. All properties are
|
||||
* optional and only exist if the service is enabled.
|
||||
*
|
||||
* @typedef {Object} Mozilla.UITour.Configuration.BrowserServices
|
||||
* @property {Mozilla.UITour.Configuration.Sync} sync - If sync is configured
|
||||
* @since 71
|
||||
*/
|
||||
|
||||
/**
|
||||
* Array of UI {@link Mozilla.UITour.Target|Targets} currently available to be annotated.
|
||||
* @typedef {Mozilla.UITour.Target[]} Mozilla.UITour.Configuration.AvailableTargets
|
||||
|
|
|
@ -24,6 +24,11 @@ ChromeUtils.defineModuleGetter(
|
|||
"CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"FxAccounts",
|
||||
|
@ -1669,6 +1674,13 @@ var UITour = {
|
|||
});
|
||||
});
|
||||
break;
|
||||
case "fxa":
|
||||
this.getFxA(aMessageManager, aCallbackID);
|
||||
break;
|
||||
|
||||
// NOTE: 'sync' is deprecated and should be removed in Firefox 73 (because
|
||||
// by then, all consumers will have upgraded to use 'fxa' in that version
|
||||
// and later.)
|
||||
case "sync":
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, {
|
||||
setup: Services.prefs.prefHasUserValue("services.sync.username"),
|
||||
|
@ -1721,6 +1733,77 @@ var UITour = {
|
|||
}
|
||||
},
|
||||
|
||||
getFxA(aMessageManager, aCallbackID) {
|
||||
(async () => {
|
||||
let setup = !!(await fxAccounts.getSignedInUser());
|
||||
let result = { setup };
|
||||
if (!setup) {
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, result);
|
||||
return;
|
||||
}
|
||||
// We are signed in so need to build a richer result.
|
||||
let devices = fxAccounts.device.recentDeviceList;
|
||||
// A recent device list is fine, but if we don't even have that we should
|
||||
// wait for it to be fetched.
|
||||
if (!devices) {
|
||||
await fxAccounts.device.refreshDeviceList();
|
||||
devices = fxAccounts.device.recentDeviceList;
|
||||
}
|
||||
if (devices) {
|
||||
// A falsey `devices` should be impossible, so we omit `devices` from
|
||||
// the result object so the consuming page can try to differentiate
|
||||
// between "no additional devices" and "something's wrong"
|
||||
result.numOtherDevices = Math.max(0, devices.length - 1);
|
||||
result.numDevicesByType = devices
|
||||
.filter(d => !d.isCurrentDevice)
|
||||
.reduce((accum, d) => {
|
||||
let type = d.type || "unknown";
|
||||
accum[type] = (accum[type] || 0) + 1;
|
||||
return accum;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Each of the "browser services" - currently only "sync" is supported.
|
||||
result.browserServices = {};
|
||||
let hasSync = Services.prefs.prefHasUserValue("services.sync.username");
|
||||
if (hasSync) {
|
||||
result.browserServices.sync = {
|
||||
// We always include 'setup' for b/w compatibility.
|
||||
setup: true,
|
||||
desktopDevices: Services.prefs.getIntPref(
|
||||
"services.sync.clients.devices.desktop",
|
||||
0
|
||||
),
|
||||
mobileDevices: Services.prefs.getIntPref(
|
||||
"services.sync.clients.devices.mobile",
|
||||
0
|
||||
),
|
||||
totalDevices: Services.prefs.getIntPref(
|
||||
"services.sync.numClients",
|
||||
0
|
||||
),
|
||||
};
|
||||
}
|
||||
// Each of the "account services", which we turn into a map keyed by ID.
|
||||
let attachedClients = await fxAccounts.listAttachedOAuthClients();
|
||||
result.accountServices = attachedClients
|
||||
.filter(c => !!c.id)
|
||||
.reduce((accum, c) => {
|
||||
accum[c.id] = {
|
||||
id: c.id,
|
||||
lastAccessedWeeksAgo: c.lastAccessedDaysAgo
|
||||
? Math.floor(c.lastAccessedDaysAgo / 7)
|
||||
: null,
|
||||
};
|
||||
return accum;
|
||||
}, {});
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, result);
|
||||
})().catch(err => {
|
||||
log.error(err);
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, {});
|
||||
});
|
||||
},
|
||||
|
||||
getAppInfo(aMessageManager, aWindow, aCallbackID) {
|
||||
(async () => {
|
||||
let appinfo = { version: Services.appinfo.version };
|
||||
|
|
|
@ -15,6 +15,7 @@ support-files =
|
|||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
[browser_fxa.js]
|
||||
skip-if = debug || asan # updateUI leaks
|
||||
[browser_fxa_config.js]
|
||||
[browser_no_tabs.js]
|
||||
[browser_openPreferences.js]
|
||||
[browser_openSearchPanel.js]
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
|
||||
var gTestTab;
|
||||
var gContentAPI;
|
||||
var gContentWindow;
|
||||
|
||||
add_task(setup_UITourTest);
|
||||
|
||||
add_UITour_task(async function test_no_user() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox.stub(fxAccounts, "getSignedInUser").returns(null);
|
||||
let result = await getConfigurationPromise("fxa");
|
||||
Assert.deepEqual(result, { setup: false });
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_no_sync_no_devices() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(fxAccounts, "getSignedInUser")
|
||||
.returns({ email: "foo@example.com" });
|
||||
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: "This Device",
|
||||
isCurrentDevice: true,
|
||||
type: "desktop",
|
||||
},
|
||||
];
|
||||
});
|
||||
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
|
||||
|
||||
let result = await getConfigurationPromise("fxa");
|
||||
Assert.deepEqual(result, {
|
||||
setup: true,
|
||||
numOtherDevices: 0,
|
||||
numDevicesByType: {},
|
||||
accountServices: {},
|
||||
browserServices: {},
|
||||
});
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_no_sync_many_devices() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(fxAccounts, "getSignedInUser")
|
||||
.returns({ email: "foo@example.com" });
|
||||
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: "This Device",
|
||||
isCurrentDevice: true,
|
||||
type: "desktop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Other Device",
|
||||
type: "mobile",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "My phone",
|
||||
type: "phone",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Who knows?",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Another desktop",
|
||||
type: "desktop",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Yet Another desktop",
|
||||
type: "desktop",
|
||||
},
|
||||
];
|
||||
});
|
||||
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
|
||||
|
||||
let result = await getConfigurationPromise("fxa");
|
||||
Assert.deepEqual(result, {
|
||||
setup: true,
|
||||
accountServices: {},
|
||||
browserServices: {},
|
||||
numOtherDevices: 5,
|
||||
numDevicesByType: {
|
||||
desktop: 2,
|
||||
mobile: 1,
|
||||
phone: 1,
|
||||
unknown: 1,
|
||||
},
|
||||
});
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_no_sync_no_cached_devices() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(fxAccounts, "getSignedInUser")
|
||||
.returns({ email: "foo@example.com" });
|
||||
let devicesStub = sandbox.stub(fxAccounts.device, "recentDeviceList");
|
||||
devicesStub.get(() => {
|
||||
// Sinon doesn't seem to support second `getters` returning a different
|
||||
// value, so replace the getter here.
|
||||
devicesStub.get(() => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: "This Device",
|
||||
isCurrentDevice: true,
|
||||
type: "desktop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Other Device",
|
||||
type: "mobile",
|
||||
},
|
||||
];
|
||||
});
|
||||
// and here we want to say "nothing is yet cached"
|
||||
return null;
|
||||
});
|
||||
|
||||
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
|
||||
let rdlStub = sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves();
|
||||
|
||||
let result = await getConfigurationPromise("fxa");
|
||||
Assert.deepEqual(result, {
|
||||
setup: true,
|
||||
accountServices: {},
|
||||
browserServices: {},
|
||||
numOtherDevices: 1,
|
||||
numDevicesByType: {
|
||||
mobile: 1,
|
||||
},
|
||||
});
|
||||
Assert.ok(rdlStub.called);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_account_clients() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(fxAccounts, "getSignedInUser")
|
||||
.returns({ email: "foo@example.com" });
|
||||
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
|
||||
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([
|
||||
{
|
||||
id: "802d56ef2a9af9fa",
|
||||
lastAccessedDaysAgo: 2,
|
||||
},
|
||||
{
|
||||
id: "1f30e32975ae5112",
|
||||
lastAccessedDaysAgo: 10,
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
name: "Some browser",
|
||||
lastAccessedDaysAgo: 10,
|
||||
},
|
||||
{
|
||||
id: "null-last-accessed",
|
||||
lastAccessedDaysAgo: null,
|
||||
},
|
||||
]);
|
||||
Assert.deepEqual(await getConfigurationPromise("fxa"), {
|
||||
setup: true,
|
||||
numOtherDevices: 0,
|
||||
numDevicesByType: {},
|
||||
accountServices: {
|
||||
"802d56ef2a9af9fa": {
|
||||
id: "802d56ef2a9af9fa",
|
||||
lastAccessedWeeksAgo: 0,
|
||||
},
|
||||
"1f30e32975ae5112": {
|
||||
id: "1f30e32975ae5112",
|
||||
lastAccessedWeeksAgo: 1,
|
||||
},
|
||||
"null-last-accessed": {
|
||||
id: "null-last-accessed",
|
||||
lastAccessedWeeksAgo: null,
|
||||
},
|
||||
},
|
||||
browserServices: {},
|
||||
});
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_sync() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(fxAccounts, "getSignedInUser")
|
||||
.returns({ email: "foo@example.com" });
|
||||
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
|
||||
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
|
||||
Services.prefs.setCharPref("services.sync.username", "tests@mozilla.org");
|
||||
Services.prefs.setIntPref("services.sync.clients.devices.desktop", 4);
|
||||
Services.prefs.setIntPref("services.sync.clients.devices.mobile", 5);
|
||||
Services.prefs.setIntPref("services.sync.numClients", 9);
|
||||
|
||||
Assert.deepEqual(await getConfigurationPromise("fxa"), {
|
||||
setup: true,
|
||||
numOtherDevices: 0,
|
||||
numDevicesByType: {},
|
||||
accountServices: {},
|
||||
browserServices: {
|
||||
sync: {
|
||||
setup: true,
|
||||
mobileDevices: 5,
|
||||
desktopDevices: 4,
|
||||
totalDevices: 9,
|
||||
},
|
||||
},
|
||||
});
|
||||
Services.prefs.clearUserPref("services.sync.username");
|
||||
Services.prefs.clearUserPref("services.sync.clients.devices.desktop");
|
||||
Services.prefs.clearUserPref("services.sync.clients.devices.mobile");
|
||||
Services.prefs.clearUserPref("services.sync.numClients");
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_UITour_task(async function test_fxa_fails() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox.stub(fxAccounts, "getSignedInUser").throws();
|
||||
let result = await getConfigurationPromise("fxa");
|
||||
Assert.deepEqual(result, {});
|
||||
sandbox.restore();
|
||||
});
|
|
@ -435,40 +435,37 @@ class FxAccounts {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an array listing all the OAuth clients
|
||||
* connected to the authenticated user's account.
|
||||
* Devices and web sessions are not included.
|
||||
* Returns an array listing all the OAuth clients connected to the
|
||||
* authenticated user's account. This includes browsers and web sessions - no
|
||||
* filtering is done of the set returned by the FxA server.
|
||||
*
|
||||
* @typedef {Object} AttachedClient
|
||||
* @property {String} id - OAuth `client_id` of the client.
|
||||
* @property {String} name - Client name. e.g. Firefox Monitor.
|
||||
* @property {Number} lastAccessTime - Last access time in milliseconds.
|
||||
* @property {Number} lastAccessedDaysAgo - How many days ago the client last
|
||||
* accessed the FxA server APIs.
|
||||
*
|
||||
* @returns {Array.<AttachedClient>} A list of attached clients.
|
||||
*/
|
||||
async listAttachedOAuthClients() {
|
||||
// We expose last accessed times in 'days ago'
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
return this._withVerifiedAccountState(async state => {
|
||||
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
|
||||
const attachedClients = await this._internal.fxAccountsClient.attachedClients(
|
||||
sessionToken
|
||||
);
|
||||
return attachedClients.reduce((oauthClients, client) => {
|
||||
// This heuristic aims to keep tokens for "associated services"
|
||||
// while throwing away the "browser" ones.
|
||||
if (
|
||||
client.clientId &&
|
||||
!client.deviceId &&
|
||||
!client.sessionTokenId &&
|
||||
client.scope
|
||||
) {
|
||||
oauthClients.push({
|
||||
id: client.clientId,
|
||||
name: client.name,
|
||||
lastAccessTime: client.lastAccessTime,
|
||||
});
|
||||
}
|
||||
return oauthClients;
|
||||
}, []);
|
||||
// We should use the server timestamp here - bug 1595635
|
||||
let now = Date.now();
|
||||
return attachedClients.map(client => {
|
||||
const daysAgo = client.lastAccessTime
|
||||
? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0)
|
||||
: null;
|
||||
return {
|
||||
id: client.clientId,
|
||||
lastAccessedDaysAgo: daysAgo,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1600,6 +1600,9 @@ add_task(async function test_getOAuthToken_authErrorRefreshesCertificate() {
|
|||
});
|
||||
|
||||
add_task(async function test_listAttachedOAuthClients() {
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
const ONE_DAY = 24 * ONE_HOUR;
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let alice = getTestUser("alice");
|
||||
alice.verified = true;
|
||||
|
@ -1607,29 +1610,14 @@ add_task(async function test_listAttachedOAuthClients() {
|
|||
let client = fxa._internal.fxAccountsClient;
|
||||
client.attachedClients = async () => {
|
||||
return [
|
||||
{
|
||||
clientId: null,
|
||||
deviceId: "deadbeef",
|
||||
sessionTokenId: "deadbeef",
|
||||
name: "Good ol' desktop device",
|
||||
scope: null,
|
||||
lastAccessTime: 1569263031001,
|
||||
},
|
||||
{
|
||||
clientId: null,
|
||||
deviceId: null,
|
||||
sessionTokenId: "deadbeef",
|
||||
name: "Mobile device w/ no device record",
|
||||
scope: null,
|
||||
lastAccessTime: 1569263031001,
|
||||
},
|
||||
// This entry was previously filtered but no longer is!
|
||||
{
|
||||
clientId: "a2270f727f45f648",
|
||||
deviceId: "deadbeef",
|
||||
sessionTokenId: null,
|
||||
name: "Firefox Preview (no session token)",
|
||||
scope: ["profile", "https://identity.mozilla.com/apps/oldsync"],
|
||||
lastAccessTime: 1569263031001,
|
||||
lastAccessTime: Date.now(),
|
||||
},
|
||||
{
|
||||
clientId: "802d56ef2a9af9fa",
|
||||
|
@ -1637,7 +1625,7 @@ add_task(async function test_listAttachedOAuthClients() {
|
|||
sessionTokenId: null,
|
||||
name: "Firefox Monitor",
|
||||
scope: ["profile"],
|
||||
lastAccessTime: 1569263031000,
|
||||
lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
|
||||
},
|
||||
{
|
||||
clientId: "1f30e32975ae5112",
|
||||
|
@ -1645,7 +1633,23 @@ add_task(async function test_listAttachedOAuthClients() {
|
|||
sessionTokenId: null,
|
||||
name: "Firefox Send",
|
||||
scope: ["profile", "https://identity.mozilla.com/apps/send"],
|
||||
lastAccessTime: 1569263013000,
|
||||
lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR,
|
||||
},
|
||||
// One with a future date should be impossible, but having a negative
|
||||
// result here would almost certainly confuse something!
|
||||
{
|
||||
clientId: "future-date",
|
||||
deviceId: null,
|
||||
sessionTokenId: null,
|
||||
name: "Whatever",
|
||||
lastAccessTime: Date.now() + ONE_DAY,
|
||||
},
|
||||
// A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo
|
||||
{
|
||||
clientId: "missing-date",
|
||||
deviceId: null,
|
||||
sessionTokenId: null,
|
||||
name: "Whatever",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@ -1653,15 +1657,25 @@ add_task(async function test_listAttachedOAuthClients() {
|
|||
await fxa.setSignedInUser(alice);
|
||||
const clients = await fxa.listAttachedOAuthClients();
|
||||
Assert.deepEqual(clients, [
|
||||
{
|
||||
id: "a2270f727f45f648",
|
||||
lastAccessedDaysAgo: 0,
|
||||
},
|
||||
{
|
||||
id: "802d56ef2a9af9fa",
|
||||
name: "Firefox Monitor",
|
||||
lastAccessTime: 1569263031000,
|
||||
lastAccessedDaysAgo: 1,
|
||||
},
|
||||
{
|
||||
id: "1f30e32975ae5112",
|
||||
name: "Firefox Send",
|
||||
lastAccessTime: 1569263013000,
|
||||
lastAccessedDaysAgo: 2,
|
||||
},
|
||||
{
|
||||
id: "future-date",
|
||||
lastAccessedDaysAgo: 0,
|
||||
},
|
||||
{
|
||||
id: "missing-date",
|
||||
lastAccessedDaysAgo: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче