richard pushed to branch tor-browser-115.7.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

14 changed files:

Changes:

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -36,6 +36,60 @@ const { TorStrings } = ChromeUtils.importESModule(
    36 36
       "resource://gre/modules/TorStrings.sys.mjs"
    
    37 37
     );
    
    38 38
     
    
    39
    +const { Lox } = ChromeUtils.importESModule(
    
    40
    +  "resource://gre/modules/Lox.sys.mjs"
    
    41
    +);
    
    42
    +
    
    43
    +/*
    
    44
    + * Fake Lox module:
    
    45
    +
    
    46
    +const Lox = {
    
    47
    +  levelHistory: [0, 1],
    
    48
    +  // levelHistory: [1, 2],
    
    49
    +  // levelHistory: [2, 3],
    
    50
    +  // levelHistory: [3, 4],
    
    51
    +  // levelHistory: [0, 1, 2],
    
    52
    +  // levelHistory: [1, 2, 3],
    
    53
    +  // levelHistory: [4, 3],
    
    54
    +  // levelHistory: [4, 1],
    
    55
    +  // levelHistory: [2, 1],
    
    56
    +  //levelHistory: [2, 3, 4, 1, 2],
    
    57
    +  // Gain some invites and then loose them all. Shouldn't show any change.
    
    58
    +  // levelHistory: [0, 1, 2, 1],
    
    59
    +  // levelHistory: [1, 2, 3, 1],
    
    60
    +  getEventData() {
    
    61
    +    let prevLevel = this.levelHistory[0];
    
    62
    +    const events = [];
    
    63
    +    for (let i = 1; i < this.levelHistory.length; i++) {
    
    64
    +      const level = this.levelHistory[i];
    
    65
    +      events.push({ type: level > prevLevel ? "levelup" : "blockage", newLevel: level });
    
    66
    +      prevLevel = level;
    
    67
    +    }
    
    68
    +    return events;
    
    69
    +  },
    
    70
    +  clearEventData() {
    
    71
    +    this.levelHistory = [];
    
    72
    +  },
    
    73
    +  nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 1 },
    
    74
    +  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 2 },
    
    75
    +  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 3 },
    
    76
    +  //nextUnlock: { date: "2024-01-31T00:00:00Z", nextLevel: 4 },
    
    77
    +  getNextUnlock() {
    
    78
    +    return this.nextUnlock;
    
    79
    +  },
    
    80
    +  remainingInvites: 3,
    
    81
    +  // remainingInvites: 0,
    
    82
    +  getRemainingInviteCount() {
    
    83
    +    return this.remainingInvites;
    
    84
    +  },
    
    85
    +  invites: [],
    
    86
    +  // invites: ["a", "b"],
    
    87
    +  getInvites() {
    
    88
    +    return this.invites;
    
    89
    +  },
    
    90
    +};
    
    91
    +*/
    
    92
    +
    
    39 93
     const InternetStatus = Object.freeze({
    
    40 94
       Unknown: 0,
    
    41 95
       Online: 1,
    
    ... ... @@ -678,15 +732,23 @@ const gBridgeGrid = {
    678 732
           row.optionsButton.focus();
    
    679 733
         });
    
    680 734
     
    
    681
    -    row.menu
    
    682
    -      .querySelector(".tor-bridges-options-qr-one-menu-item")
    
    683
    -      .addEventListener("click", () => {
    
    684
    -        const bridgeLine = row.bridgeLine;
    
    685
    -        if (!bridgeLine) {
    
    686
    -          return;
    
    687
    -        }
    
    688
    -        showBridgeQr(bridgeLine);
    
    689
    -      });
    
    735
    +    const qrItem = row.menu.querySelector(
    
    736
    +      ".tor-bridges-options-qr-one-menu-item"
    
    737
    +    );
    
    738
    +    row.menu.addEventListener("showing", () => {
    
    739
    +      qrItem.hidden = !(
    
    740
    +        this._bridgeSource === TorBridgeSource.UserProvided ||
    
    741
    +        this._bridgeSource === TorBridgeSource.BridgeDB
    
    742
    +      );
    
    743
    +    });
    
    744
    +
    
    745
    +    qrItem.addEventListener("click", () => {
    
    746
    +      const bridgeLine = row.bridgeLine;
    
    747
    +      if (!bridgeLine) {
    
    748
    +        return;
    
    749
    +      }
    
    750
    +      showBridgeQr(bridgeLine);
    
    751
    +    });
    
    690 752
         row.menu
    
    691 753
           .querySelector(".tor-bridges-options-copy-one-menu-item")
    
    692 754
           .addEventListener("click", () => {
    
    ... ... @@ -734,7 +796,11 @@ const gBridgeGrid = {
    734 796
        *
    
    735 797
        * @type {string[]}
    
    736 798
        */
    
    737
    -  _supportedSources: [TorBridgeSource.BridgeDB, TorBridgeSource.UserProvided],
    
    799
    +  _supportedSources: [
    
    800
    +    TorBridgeSource.BridgeDB,
    
    801
    +    TorBridgeSource.UserProvided,
    
    802
    +    TorBridgeSource.Lox,
    
    803
    +  ],
    
    738 804
     
    
    739 805
       /**
    
    740 806
        * Update the grid to show the latest bridge strings.
    
    ... ... @@ -1169,6 +1235,335 @@ const gBuiltinBridgesArea = {
    1169 1235
       },
    
    1170 1236
     };
    
    1171 1237
     
    
    1238
    +/**
    
    1239
    + * Controls the bridge pass area.
    
    1240
    + */
    
    1241
    +const gLoxStatus = {
    
    1242
    +  /**
    
    1243
    +   * The status area.
    
    1244
    +   *
    
    1245
    +   * @type {Element?}
    
    1246
    +   */
    
    1247
    +  _area: null,
    
    1248
    +  /**
    
    1249
    +   * The area for showing the next unlock and invites.
    
    1250
    +   *
    
    1251
    +   * @type {Element?}
    
    1252
    +   */
    
    1253
    +  _detailsArea: null,
    
    1254
    +  /**
    
    1255
    +   * The day counter for the next unlock.
    
    1256
    +   *
    
    1257
    +   * @type {Element?}
    
    1258
    +   */
    
    1259
    +  _nextUnlockCounterEl: null,
    
    1260
    +  /**
    
    1261
    +   * Shows the number of remaining invites.
    
    1262
    +   *
    
    1263
    +   * @type {Element?}
    
    1264
    +   */
    
    1265
    +  _remainingInvitesEl: null,
    
    1266
    +  /**
    
    1267
    +   * The button to show the invites.
    
    1268
    +   *
    
    1269
    +   * @type {Element?}
    
    1270
    +   */
    
    1271
    +  _invitesButton: null,
    
    1272
    +  /**
    
    1273
    +   * The alert for new unlocks.
    
    1274
    +   *
    
    1275
    +   * @type {Element?}
    
    1276
    +   */
    
    1277
    +  _unlockAlert: null,
    
    1278
    +  /**
    
    1279
    +   * The alert title.
    
    1280
    +   *
    
    1281
    +   * @type {Element?}
    
    1282
    +   */
    
    1283
    +  _unlockAlertTitle: null,
    
    1284
    +  /**
    
    1285
    +   * The alert invites item.
    
    1286
    +   *
    
    1287
    +   * @type {Element?}
    
    1288
    +   */
    
    1289
    +  _unlockAlertInvitesItem: null,
    
    1290
    +  /**
    
    1291
    +   * Button for the user to dismiss the alert.
    
    1292
    +   *
    
    1293
    +   * @type {Element?}
    
    1294
    +   */
    
    1295
    +  _unlockAlertButton: null,
    
    1296
    +
    
    1297
    +  /**
    
    1298
    +   * Initialize the bridge pass area.
    
    1299
    +   */
    
    1300
    +  init() {
    
    1301
    +    this._area = document.getElementById("tor-bridges-lox-status");
    
    1302
    +    this._detailsArea = document.getElementById("tor-bridges-lox-details");
    
    1303
    +    this._nextUnlockCounterEl = document.getElementById(
    
    1304
    +      "tor-bridges-lox-next-unlock-counter"
    
    1305
    +    );
    
    1306
    +    this._remainingInvitesEl = document.getElementById(
    
    1307
    +      "tor-bridges-lox-remaining-invites"
    
    1308
    +    );
    
    1309
    +    this._invitesButton = document.getElementById(
    
    1310
    +      "tor-bridges-lox-show-invites-button"
    
    1311
    +    );
    
    1312
    +    this._unlockAlert = document.getElementById("tor-bridges-lox-unlock-alert");
    
    1313
    +    this._unlockAlertTitle = document.getElementById(
    
    1314
    +      "tor-bridge-unlock-alert-title"
    
    1315
    +    );
    
    1316
    +    this._unlockAlertInviteItem = document.getElementById(
    
    1317
    +      "tor-bridges-lox-unlock-alert-invites"
    
    1318
    +    );
    
    1319
    +    this._unlockAlertButton = document.getElementById(
    
    1320
    +      "tor-bridges-lox-unlock-alert-button"
    
    1321
    +    );
    
    1322
    +
    
    1323
    +    this._invitesButton.addEventListener("click", () => {
    
    1324
    +      // TODO: Show invites.
    
    1325
    +    });
    
    1326
    +    this._unlockAlertButton.addEventListener("click", () => {
    
    1327
    +      // TODO: Have a way to ensure that the cleared event data matches the
    
    1328
    +      // current _loxId
    
    1329
    +      Lox.clearEventData();
    
    1330
    +      // TODO: Listen for events from Lox, rather than call _updateUnlocks
    
    1331
    +      // directly.
    
    1332
    +      this._updateUnlocks();
    
    1333
    +    });
    
    1334
    +
    
    1335
    +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    1336
    +    // TODO: Listen for new events from Lox, when it is supported.
    
    1337
    +
    
    1338
    +    // NOTE: Before initializedPromise completes, this area is hidden.
    
    1339
    +    TorSettings.initializedPromise.then(() => {
    
    1340
    +      this._updateLoxId();
    
    1341
    +    });
    
    1342
    +  },
    
    1343
    +
    
    1344
    +  /**
    
    1345
    +   * Uninitialize the built-in bridges area.
    
    1346
    +   */
    
    1347
    +  uninit() {
    
    1348
    +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    1349
    +  },
    
    1350
    +
    
    1351
    +  observe(subject, topic, data) {
    
    1352
    +    switch (topic) {
    
    1353
    +      case TorSettingsTopics.SettingsChanged:
    
    1354
    +        const { changes } = subject.wrappedJSObject;
    
    1355
    +        if (
    
    1356
    +          changes.includes("bridges.source") ||
    
    1357
    +          changes.includes("bridges.lox_id")
    
    1358
    +        ) {
    
    1359
    +          this._updateLoxId();
    
    1360
    +        }
    
    1361
    +        break;
    
    1362
    +    }
    
    1363
    +  },
    
    1364
    +
    
    1365
    +  /**
    
    1366
    +   * The Lox id currently shown. Empty if deactivated, and null if
    
    1367
    +   * uninitialized.
    
    1368
    +   *
    
    1369
    +   * @type {string?}
    
    1370
    +   */
    
    1371
    +  _loxId: null,
    
    1372
    +
    
    1373
    +  /**
    
    1374
    +   * Update the shown bridge pass.
    
    1375
    +   */
    
    1376
    +  async _updateLoxId() {
    
    1377
    +    let loxId =
    
    1378
    +      TorSettings.bridges.source === TorBridgeSource.Lox
    
    1379
    +        ? TorSettings.bridges.lox_id
    
    1380
    +        : "";
    
    1381
    +    if (loxId !== this._loxId) {
    
    1382
    +      this._loxId = loxId;
    
    1383
    +      this._updateUnlocks();
    
    1384
    +      this._updateInvites();
    
    1385
    +    }
    
    1386
    +  },
    
    1387
    +
    
    1388
    +  /**
    
    1389
    +   * Update the display of the current or next unlock.
    
    1390
    +   */
    
    1391
    +  async _updateUnlocks() {
    
    1392
    +    // Cache the loxId before we await.
    
    1393
    +    const loxId = this._loxId;
    
    1394
    +
    
    1395
    +    if (!loxId) {
    
    1396
    +      // NOTE: This area should already be hidden by the change in Lox source,
    
    1397
    +      // but we clean up for the next non-empty id.
    
    1398
    +      this._area.classList.remove("show-unlock-alert");
    
    1399
    +      this._area.classList.remove("show-next-unlock");
    
    1400
    +      return;
    
    1401
    +    }
    
    1402
    +
    
    1403
    +    let pendingEvents;
    
    1404
    +    let nextUnlock;
    
    1405
    +    let numInvites;
    
    1406
    +    // Fetch the latest events or details about the next unlock.
    
    1407
    +    try {
    
    1408
    +      nextUnlock = await Lox.getNextUnlock();
    
    1409
    +      pendingEvents = Lox.getEventData();
    
    1410
    +      numInvites = Lox.getRemainingInviteCount();
    
    1411
    +    } catch (e) {
    
    1412
    +      console.error("Failed get get lox updates", e);
    
    1413
    +      return;
    
    1414
    +    }
    
    1415
    +
    
    1416
    +    if (loxId !== this._loxId) {
    
    1417
    +      // Replaced during await.
    
    1418
    +      return;
    
    1419
    +    }
    
    1420
    +
    
    1421
    +    // Grab focus state before changing visibility.
    
    1422
    +    const alertHadFocus = this._unlockAlert.contains(document.activeElement);
    
    1423
    +    const detailsHadFocus = this._detailsArea.contains(document.activeElement);
    
    1424
    +
    
    1425
    +    const showAlert = !!pendingEvents.length;
    
    1426
    +    this._area.classList.toggle("show-unlock-alert", showAlert);
    
    1427
    +    this._area.classList.toggle("show-next-unlock", !showAlert);
    
    1428
    +
    
    1429
    +    if (showAlert) {
    
    1430
    +      // At level 0 and level 1, we do not have any invites.
    
    1431
    +      // If the user starts and ends on level 0 or 1, then overall they would
    
    1432
    +      // have had no change in their invites. So we do not want to show their
    
    1433
    +      // latest updates.
    
    1434
    +      // NOTE: If the user starts at level > 1 and ends with level 1 (levelling
    
    1435
    +      // down to level 0 should not be possible), then we *do* want to show the
    
    1436
    +      // user that they now have "0" invites.
    
    1437
    +      // NOTE: pendingEvents are time-ordered, with the most recent event
    
    1438
    +      // *last*.
    
    1439
    +      const firstEvent = pendingEvents[0];
    
    1440
    +      // NOTE: We cannot get a blockage event when the user starts at level 1 or
    
    1441
    +      // 0.
    
    1442
    +      const startingAtLowLevel =
    
    1443
    +        firstEvent.type === "levelup" && firstEvent.newLevel <= 2;
    
    1444
    +      const lastEvent = pendingEvents[pendingEvents.length - 1];
    
    1445
    +      const endingAtLowLevel = lastEvent.newLevel <= 1;
    
    1446
    +
    
    1447
    +      const showInvites = !(startingAtLowLevel && endingAtLowLevel);
    
    1448
    +
    
    1449
    +      let blockage = false;
    
    1450
    +      let levelUp = false;
    
    1451
    +      let bridgeGain = false;
    
    1452
    +      // Go through events, in the order that they occurred.
    
    1453
    +      for (const loxEvent of pendingEvents) {
    
    1454
    +        if (loxEvent.type === "levelup") {
    
    1455
    +          levelUp = true;
    
    1456
    +          if (loxEvent.newLevel === 1) {
    
    1457
    +            // Gain 2 bridges from level 0 to 1.
    
    1458
    +            bridgeGain = true;
    
    1459
    +          }
    
    1460
    +        } else {
    
    1461
    +          blockage = true;
    
    1462
    +        }
    
    1463
    +      }
    
    1464
    +      let alertTitleId;
    
    1465
    +      if (levelUp && !blockage) {
    
    1466
    +        alertTitleId = "tor-bridges-lox-upgrade";
    
    1467
    +      } else {
    
    1468
    +        // Show as blocked bridges replaced.
    
    1469
    +        // Even if we have a mixture of level ups as well.
    
    1470
    +        alertTitleId = "tor-bridges-lox-blocked";
    
    1471
    +      }
    
    1472
    +      document.l10n.setAttributes(this._unlockAlertTitle, alertTitleId);
    
    1473
    +      document.l10n.setAttributes(
    
    1474
    +        this._unlockAlertInviteItem,
    
    1475
    +        "tor-bridges-lox-new-invites",
    
    1476
    +        { numInvites }
    
    1477
    +      );
    
    1478
    +      this._unlockAlert.classList.toggle(
    
    1479
    +        "lox-unlock-upgrade",
    
    1480
    +        levelUp && !blockage
    
    1481
    +      );
    
    1482
    +      this._unlockAlert.classList.toggle("lox-unlock-new-bridges", blockage);
    
    1483
    +      this._unlockAlert.classList.toggle("lox-unlock-gain-bridges", bridgeGain);
    
    1484
    +      this._unlockAlert.classList.toggle("lox-unlock-invites", showInvites);
    
    1485
    +    } else {
    
    1486
    +      // Show next unlock.
    
    1487
    +      // Number of days until the next unlock, rounded up.
    
    1488
    +      const numDays = Math.max(
    
    1489
    +        1,
    
    1490
    +        Math.ceil(
    
    1491
    +          (new Date(nextUnlock.date).getTime() - Date.now()) /
    
    1492
    +            (24 * 60 * 60 * 1000)
    
    1493
    +        )
    
    1494
    +      );
    
    1495
    +      document.l10n.setAttributes(
    
    1496
    +        this._nextUnlockCounterEl,
    
    1497
    +        "tor-bridges-lox-days-until-unlock",
    
    1498
    +        { numDays }
    
    1499
    +      );
    
    1500
    +
    
    1501
    +      // Gain 2 bridges from level 0 to 1. After that gain invites.
    
    1502
    +      const bridgeGain = nextUnlock.nextLevel === 1;
    
    1503
    +      const firstInvites = nextUnlock.nextLevel === 2;
    
    1504
    +      const moreInvites = nextUnlock.nextLevel > 2;
    
    1505
    +
    
    1506
    +      this._detailsArea.classList.toggle("lox-next-gain-bridges", bridgeGain);
    
    1507
    +      this._detailsArea.classList.toggle(
    
    1508
    +        "lox-next-first-invites",
    
    1509
    +        firstInvites
    
    1510
    +      );
    
    1511
    +      this._detailsArea.classList.toggle("lox-next-more-invites", moreInvites);
    
    1512
    +    }
    
    1513
    +
    
    1514
    +    if (alertHadFocus && !showAlert) {
    
    1515
    +      // Has become hidden.
    
    1516
    +      this._nextUnlockCounterEl.focus();
    
    1517
    +    } else if (detailsHadFocus && showAlert) {
    
    1518
    +      this._unlockAlertButton.focus();
    
    1519
    +    }
    
    1520
    +  },
    
    1521
    +
    
    1522
    +  /**
    
    1523
    +   * Update the invites area.
    
    1524
    +   */
    
    1525
    +  _updateInvites() {
    
    1526
    +    if (!this._loxId) {
    
    1527
    +      return;
    
    1528
    +    }
    
    1529
    +
    
    1530
    +    let remainingInvites;
    
    1531
    +    let existingInvites;
    
    1532
    +    // Fetch the latest events or details about the next unlock.
    
    1533
    +    try {
    
    1534
    +      remainingInvites = Lox.getRemainingInviteCount();
    
    1535
    +      existingInvites = Lox.getInvites().length;
    
    1536
    +    } catch (e) {
    
    1537
    +      console.error("Failed get get remaining invites", e);
    
    1538
    +      return;
    
    1539
    +    }
    
    1540
    +
    
    1541
    +    const hasInvites = !!existingInvites || !!remainingInvites;
    
    1542
    +
    
    1543
    +    if (!hasInvites) {
    
    1544
    +      if (
    
    1545
    +        this._remainingInvitesEl.contains(document.activeElement) ||
    
    1546
    +        this._invitesButton.contains(document.activeElement)
    
    1547
    +      ) {
    
    1548
    +        // About to loose focus.
    
    1549
    +        // Unexpected for the lox level to loose all invites.
    
    1550
    +        // Move to the top of the details area, which should be visible if we
    
    1551
    +        // just had focus.
    
    1552
    +        this._nextUnlockCounterEl.focus();
    
    1553
    +      }
    
    1554
    +    }
    
    1555
    +    // Hide the invite elements if we have no historic invites or a way of
    
    1556
    +    // creating new ones.
    
    1557
    +    this._detailsArea.classList.toggle("lox-has-invites", hasInvites);
    
    1558
    +
    
    1559
    +    document.l10n.setAttributes(
    
    1560
    +      this._remainingInvitesEl,
    
    1561
    +      "tor-bridges-lox-remaining-invites",
    
    1562
    +      { numInvites: remainingInvites }
    
    1563
    +    );
    
    1564
    +  },
    
    1565
    +};
    
    1566
    +
    
    1172 1567
     /**
    
    1173 1568
      * Controls the bridge settings.
    
    1174 1569
      */
    
    ... ... @@ -1295,6 +1690,7 @@ const gBridgeSettings = {
    1295 1690
     
    
    1296 1691
         gBridgeGrid.init();
    
    1297 1692
         gBuiltinBridgesArea.init();
    
    1693
    +    gLoxStatus.init();
    
    1298 1694
     
    
    1299 1695
         this._initBridgesMenu();
    
    1300 1696
         this._initShareArea();
    
    ... ... @@ -1315,6 +1711,7 @@ const gBridgeSettings = {
    1315 1711
       uninit() {
    
    1316 1712
         gBridgeGrid.uninit();
    
    1317 1713
         gBuiltinBridgesArea.uninit();
    
    1714
    +    gLoxStatus.uninit();
    
    1318 1715
     
    
    1319 1716
         Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    1320 1717
       },
    
    ... ... @@ -1387,6 +1784,10 @@ const gBridgeSettings = {
    1387 1784
           "source-requested",
    
    1388 1785
           bridgeSource === TorBridgeSource.BridgeDB
    
    1389 1786
         );
    
    1787
    +    this._bridgesEl.classList.toggle(
    
    1788
    +      "source-lox",
    
    1789
    +      bridgeSource === TorBridgeSource.Lox
    
    1790
    +    );
    
    1390 1791
     
    
    1391 1792
         // Force the menu to close whenever the source changes.
    
    1392 1793
         // NOTE: If the menu had focus then hadFocus will be true, and focus will be
    
    ... ... @@ -1615,7 +2016,10 @@ const gBridgeSettings = {
    1615 2016
     
    
    1616 2017
         this._bridgesMenu.addEventListener("showing", () => {
    
    1617 2018
           const canCopy = this._bridgeSource !== TorBridgeSource.BuiltIn;
    
    1618
    -      qrItem.hidden = !this._canQRBridges || !canCopy;
    
    2019
    +      const canShare =
    
    2020
    +        this._bridgeSource === TorBridgeSource.UserProvided ||
    
    2021
    +        this._bridgeSource === TorBridgeSource.BridgeDB;
    
    2022
    +      qrItem.hidden = !canShare || !this._canQRBridges;
    
    1619 2023
           copyItem.hidden = !canCopy;
    
    1620 2024
           editItem.hidden = this._bridgeSource !== TorBridgeSource.UserProvided;
    
    1621 2025
         });
    
    ... ... @@ -1763,13 +2167,19 @@ const gBridgeSettings = {
    1763 2167
           "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
    
    1764 2168
           { mode },
    
    1765 2169
           result => {
    
    1766
    -        if (!result.bridges?.length) {
    
    2170
    +        const loxId = result.loxId;
    
    2171
    +        if (!loxId && !result.addresses?.length) {
    
    1767 2172
               return null;
    
    1768 2173
             }
    
    1769 2174
             return setTorSettings(() => {
    
    1770 2175
               TorSettings.bridges.enabled = true;
    
    1771
    -          TorSettings.bridges.source = TorBridgeSource.UserProvided;
    
    1772
    -          TorSettings.bridges.bridge_strings = result.bridges;
    
    2176
    +          if (loxId) {
    
    2177
    +            TorSettings.bridges.source = TorBridgeSource.Lox;
    
    2178
    +            TorSettings.bridges.lox_id = loxId;
    
    2179
    +          } else {
    
    2180
    +            TorSettings.bridges.source = TorBridgeSource.UserProvided;
    
    2181
    +            TorSettings.bridges.bridge_strings = result.addresses;
    
    2182
    +          }
    
    1773 2183
             });
    
    1774 2184
           }
    
    1775 2185
         );
    

  • browser/components/torpreferences/content/connectionPane.xhtml
    ... ... @@ -172,6 +172,10 @@
    172 172
               id="tor-bridges-requested-label"
    
    173 173
               data-l10n-id="tor-bridges-source-requested"
    
    174 174
             ></html:span>
    
    175
    +        <html:span id="tor-bridges-lox-label">
    
    176
    +          <html:img id="tor-bridges-lox-label-icon" alt="" />
    
    177
    +          <html:span data-l10n-id="tor-bridges-source-lox"></html:span>
    
    178
    +        </html:span>
    
    175 179
             <html:button
    
    176 180
               id="tor-bridges-all-options-button"
    
    177 181
               class="tor-bridges-options-button"
    
    ... ... @@ -275,7 +279,7 @@
    275 279
               </html:span>
    
    276 280
             </html:div>
    
    277 281
           </html:template>
    
    278
    -      <html:div id="tor-bridges-share">
    
    282
    +      <html:div id="tor-bridges-share" class="tor-bridges-details-box">
    
    279 283
             <html:h3
    
    280 284
               id="tor-bridges-share-heading"
    
    281 285
               data-l10n-id="tor-bridges-share-heading"
    
    ... ... @@ -293,6 +297,77 @@
    293 297
               data-l10n-id="tor-bridges-qr-addresses-button"
    
    294 298
             ></html:button>
    
    295 299
           </html:div>
    
    300
    +      <html:div id="tor-bridges-lox-status">
    
    301
    +        <html:div data-l10n-id="tor-bridges-lox-description"></html:div>
    
    302
    +        <html:div
    
    303
    +          id="tor-bridges-lox-details"
    
    304
    +          class="tor-bridges-details-box tor-bridges-lox-box"
    
    305
    +        >
    
    306
    +          <html:img alt="" class="tor-bridges-lox-image-inner" />
    
    307
    +          <html:img alt="" class="tor-bridges-lox-image-outer" />
    
    308
    +          <html:div
    
    309
    +            id="tor-bridges-lox-next-unlock-counter"
    
    310
    +            class="tor-bridges-lox-intro"
    
    311
    +            tabindex="-1"
    
    312
    +          ></html:div>
    
    313
    +          <html:ul class="tor-bridges-lox-list">
    
    314
    +            <html:li
    
    315
    +              id="tor-bridges-lox-next-unlock-gain-bridges"
    
    316
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    317
    +              data-l10n-id="tor-bridges-lox-unlock-two-bridges"
    
    318
    +            ></html:li>
    
    319
    +            <html:li
    
    320
    +              id="tor-bridges-lox-next-unlock-first-invites"
    
    321
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    322
    +              data-l10n-id="tor-bridges-lox-unlock-first-invites"
    
    323
    +            ></html:li>
    
    324
    +            <html:li
    
    325
    +              id="tor-bridges-lox-next-unlock-more-invites"
    
    326
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    327
    +              data-l10n-id="tor-bridges-lox-unlock-more-invites"
    
    328
    +            ></html:li>
    
    329
    +          </html:ul>
    
    330
    +          <html:div id="tor-bridges-lox-remaining-invites"></html:div>
    
    331
    +          <html:button
    
    332
    +            id="tor-bridges-lox-show-invites-button"
    
    333
    +            class="tor-bridges-lox-button"
    
    334
    +            data-l10n-id="tor-bridges-lox-show-invites-button"
    
    335
    +          ></html:button>
    
    336
    +        </html:div>
    
    337
    +        <html:div
    
    338
    +          id="tor-bridges-lox-unlock-alert"
    
    339
    +          role="alert"
    
    340
    +          class="tor-bridges-details-box tor-bridges-lox-box"
    
    341
    +        >
    
    342
    +          <html:img alt="" class="tor-bridges-lox-image-inner" />
    
    343
    +          <html:img alt="" class="tor-bridges-lox-image-outer" />
    
    344
    +          <html:div
    
    345
    +            id="tor-bridge-unlock-alert-title"
    
    346
    +            class="tor-bridges-lox-intro"
    
    347
    +          ></html:div>
    
    348
    +          <html:ul class="tor-bridges-lox-list">
    
    349
    +            <html:li
    
    350
    +              id="tor-bridges-lox-unlock-alert-gain-bridges"
    
    351
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    352
    +              data-l10n-id="tor-bridges-lox-gained-two-bridges"
    
    353
    +            ></html:li>
    
    354
    +            <html:li
    
    355
    +              id="tor-bridges-lox-unlock-alert-new-bridges"
    
    356
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    357
    +              data-l10n-id="tor-bridges-lox-new-bridges"
    
    358
    +            ></html:li>
    
    359
    +            <html:li
    
    360
    +              id="tor-bridges-lox-unlock-alert-invites"
    
    361
    +              class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    362
    +            ></html:li>
    
    363
    +          </html:ul>
    
    364
    +          <html:button
    
    365
    +            id="tor-bridges-lox-unlock-alert-button"
    
    366
    +            class="tor-bridges-lox-button"
    
    367
    +            data-l10n-id="tor-bridges-lox-got-it-button"
    
    368
    +          ></html:button>
    
    369
    +        </html:div>
    
    370
    +      </html:div>
    
    296 371
         </html:div>
    
    297 372
         <html:h2 id="tor-bridges-change-heading"></html:h2>
    
    298 373
         <hbox align="center">
    

  • browser/components/torpreferences/content/lox-bridge-icon.svg
    1
    +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +<rect x="0.5" y="0.5" width="17" height="17" rx="1.5" stroke="context-stroke"/>
    
    3
    +<path d="M14.625 11.625C14.625 8.5184 12.1066 6 9 6C5.8934 6 3.375 8.5184 3.375 11.625V12.5596H4.25391V11.625C4.25391 9.0038 6.3788 6.87891 9 6.87891C11.6212 6.87891 13.7461 9.0038 13.7461 11.625V12.5596H14.625V11.625Z" fill="context-fill"/>
    
    4
    +<path d="M12.9375 11.625C12.9375 9.45037 11.1746 7.6875 9 7.6875C6.82537 7.6875 5.0625 9.45037 5.0625 11.625L5.06242 12.5596H5.94132L5.9414 11.625C5.9414 9.93578 7.31078 8.5664 9 8.5664C10.6892 8.5664 12.0586 9.93578 12.0586 11.625L12.0587 12.5596H12.9376L12.9375 11.625Z" fill="context-fill"/>
    
    5
    +<path d="M9 9.375C10.2426 9.375 11.25 10.3824 11.25 11.625L11.2502 12.5596H10.3713L10.3711 11.625C10.3711 10.8678 9.75724 10.2539 9 10.2539C8.24277 10.2539 7.62891 10.8678 7.62891 11.625L7.62873 12.5596H6.74982V11.625C6.74982 10.3824 7.75736 9.375 9 9.375Z" fill="context-fill"/>
    
    6
    +</svg>

  • browser/components/torpreferences/content/lox-bridge-pass.svg
    1
    +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +  <path fill-rule="evenodd" clip-rule="evenodd" d="M0.825958 10.1991C0.191356 9.56445 0.191356 8.53556 0.825958 7.90095L7.89702 0.829887C8.53163 0.195285 9.56052 0.195285 10.1951 0.829887L11.3198 1.95455C11.5751 2.2099 11.6412 2.59873 11.4846 2.92409C11.0031 3.9241 12.0507 4.97168 13.0507 4.4902C13.376 4.33354 13.7649 4.39962 14.0202 4.65497L15.1449 5.77963C15.7795 6.41424 15.7795 7.44313 15.1449 8.07773L8.0738 15.1488C7.4392 15.7834 6.41031 15.7834 5.7757 15.1488L4.65104 14.0241C4.39569 13.7688 4.32961 13.38 4.48627 13.0546C4.96775 12.0546 3.92017 11.007 2.92016 11.4885C2.5948 11.6451 2.20597 11.5791 1.95062 11.3237L0.825958 10.1991ZM1.70984 8.78484C1.56339 8.93128 1.56339 9.16872 1.70984 9.31517L2.64555 10.2509C4.53493 9.58573 6.38902 11.4398 5.72388 13.3292L6.65959 14.2649C6.80603 14.4114 7.04347 14.4114 7.18992 14.2649L14.261 7.19385C14.4074 7.0474 14.4074 6.80996 14.261 6.66352L13.3253 5.7278C11.4359 6.39295 9.5818 4.53886 10.247 2.64948L9.31124 1.71377C9.16479 1.56732 8.92735 1.56732 8.78091 1.71377L1.70984 8.78484Z" fill="context-fill"/>
    
    3
    +  <path d="M8.87637 9.78539L9.76025 8.90151L10.6441 9.78539L9.76025 10.6693L8.87637 9.78539Z" fill="context-fill"/>
    
    4
    +  <path d="M7.1086 8.01763L7.99249 7.13374L8.87637 8.01763L7.99249 8.90151L7.1086 8.01763Z" fill="context-fill"/>
    
    5
    +  <path d="M5.34084 6.24986L6.22472 5.36598L7.1086 6.24986L6.22472 7.13374L5.34084 6.24986Z" fill="context-fill"/>
    
    6
    +</svg>

  • browser/components/torpreferences/content/lox-complete-ring.svg
    1
    +<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +<defs>
    
    3
    +  <filter id="ring-blur" x="0" y="0" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    
    4
    +    <feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0"/>
    
    5
    +    <feGaussianBlur stdDeviation="2"/>
    
    6
    +    <feBlend mode="normal" in="SourceGraphic"/>
    
    7
    +  </filter>
    
    8
    +</defs>
    
    9
    +<g filter="url(#ring-blur)">
    
    10
    +<path d="M40 22C40 31.9411 31.9411 40 22 40C12.0589 40 4 31.9411 4 22C4 12.0589 12.0589 4 22 4C31.9411 4 40 12.0589 40 22ZM6.25 22C6.25 30.6985 13.3015 37.75 22 37.75C30.6985 37.75 37.75 30.6985 37.75 22C37.75 13.3015 30.6985 6.25 22 6.25C13.3015 6.25 6.25 13.3015 6.25 22Z" fill="context-fill"/>
    
    11
    +</g>
    
    12
    +</svg>

  • browser/components/torpreferences/content/lox-invite-icon.svg
    1
    +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +<rect x="0.5" y="0.5" width="17" height="17" rx="1.5" stroke="context-stroke"/>
    
    3
    +<path d="M9 9C8.45 9 7.97917 8.80417 7.5875 8.4125C7.19583 8.02083 7 7.55 7 7C7 6.45 7.19583 5.97917 7.5875 5.5875C7.97917 5.19583 8.45 5 9 5C9.55 5 10.0208 5.19583 10.4125 5.5875C10.8042 5.97917 11 6.45 11 7C11 7.55 10.8042 8.02083 10.4125 8.4125C10.0208 8.80417 9.55 9 9 9ZM5 12V11.6C5 11.3167 5.07292 11.0563 5.21875 10.8187C5.36458 10.5812 5.55833 10.4 5.8 10.275C6.31667 10.0167 6.84167 9.82292 7.375 9.69375C7.90833 9.56458 8.45 9.5 9 9.5C9.55 9.5 10.0917 9.56458 10.625 9.69375C11.1583 9.82292 11.6833 10.0167 12.2 10.275C12.4417 10.4 12.6354 10.5812 12.7812 10.8187C12.9271 11.0563 13 11.3167 13 11.6V12C13 12.275 12.9021 12.5104 12.7063 12.7063C12.5104 12.9021 12.275 13 12 13H6C5.725 13 5.48958 12.9021 5.29375 12.7063C5.09792 12.5104 5 12.275 5 12ZM6 12H12V11.6C12 11.5083 11.9771 11.425 11.9313 11.35C11.8854 11.275 11.825 11.2167 11.75 11.175C11.3 10.95 10.8458 10.7813 10.3875 10.6688C9.92917 10.5563 9.46667 10.5 9 10.5C8.53333 10.5 8.07083 10.5563 7.6125 10.6688C7.15417 10.7813 6.7 10.95 6.25 11.175C6.175 11.2167 6.11458 11.275 6.06875 11.35C6.02292 11.425 6 11.5083 6 11.6V12ZM9 8C9.275 8 9.51042 7.90208 9.70625 7.70625C9.90208 7.51042 10 7.275 10 7C10 6.725 9.90208 6.48958 9.70625 6.29375C9.51042 6.09792 9.275 6 9 6C8.725 6 8.48958 6.09792 8.29375 6.29375C8.09792 6.48958 8 6.725 8 7C8 7.275 8.09792 7.51042 8.29375 7.70625C8.48958 7.90208 8.725 8 9 8Z" fill="context-fill"/>
    
    4
    +</svg>

  • browser/components/torpreferences/content/lox-progress-ring.svg
    1
    +<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +<defs>
    
    3
    +  <filter id="ring-blur" x="0" y="0" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    
    4
    +    <feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0"/>
    
    5
    +    <feGaussianBlur stdDeviation="2"/>
    
    6
    +    <feBlend mode="normal" in="SourceGraphic"/>
    
    7
    +  </filter>
    
    8
    +</defs>
    
    9
    +<path d="M 40,22 C 40,31.9411 31.9411,40 22,40 12.05887,40 4,31.9411 4,22 4,12.0589 12.05887,4 22,4 31.9411,4 40,12.0589 40,22 Z M 6.25,22 c 0,8.6985 7.05152,15.75 15.75,15.75 8.6985,0 15.75,-7.0515 15.75,-15.75 C 37.75,13.3015 30.6985,6.25 22,6.25 13.30152,6.25 6.25,13.3015 6.25,22 Z" fill="context-stroke"/>
    
    10
    +<g filter="url(#ring-blur)">
    
    11
    +  <path d="m 22,5.125 c 0,-0.62132 0.5042,-1.12866 1.1243,-1.08986 3.0298,0.18958 5.963,1.14263 8.5256,2.77013 0.5245,0.3331 0.6342,1.03991 0.269,1.54257 C 31.5537,8.8505 30.852,8.95814 30.3246,8.62974 28.1505,7.27609 25.6786,6.47291 23.1241,6.29015 22.5044,6.24582 22,5.74632 22,5.125 Z" fill="context-fill"/>
    
    12
    +</g>
    
    13
    +</svg>

  • browser/components/torpreferences/content/lox-success.svg
    1
    +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
    
    2
    +  <path d="M10 14C9.62059 14.0061 9.25109 13.8787 8.95594 13.6403C8.6608 13.4018 8.45874 13.0672 8.385 12.695L8.028 11.028C7.97202 10.7692 7.84264 10.5321 7.65536 10.345C7.46807 10.1579 7.23081 10.0287 6.972 9.973L5.305 9.616C4.524 9.447 4 8.798 4 8C4 7.202 4.524 6.553 5.305 6.385L6.972 6.028C7.23078 5.97185 7.46794 5.84243 7.65519 5.65519C7.84243 5.46794 7.97185 5.23078 8.028 4.972L8.386 3.305C8.553 2.524 9.202 2 10 2C10.798 2 11.447 2.524 11.615 3.305L11.972 4.972C12.086 5.5 12.5 5.914 13.028 6.027L14.695 6.384C15.476 6.553 16 7.202 16 8C16 8.798 15.476 9.447 14.695 9.615L13.028 9.972C12.769 10.028 12.5316 10.1575 12.3443 10.345C12.157 10.5324 12.0277 10.7699 11.972 11.029L11.614 12.695C11.5407 13.0672 11.3388 13.4019 11.0438 13.6404C10.7488 13.8789 10.3793 14.0062 10 14ZM10 3.25C9.902 3.25 9.669 3.28 9.608 3.566L9.25 5.234C9.14333 5.72828 8.89643 6.18132 8.53888 6.53888C8.18132 6.89643 7.72828 7.14333 7.234 7.25L5.567 7.607C5.281 7.669 5.25 7.902 5.25 8C5.25 8.098 5.281 8.331 5.567 8.393L7.234 8.75C7.72828 8.85667 8.18132 9.10357 8.53888 9.46112C8.89643 9.81868 9.14333 10.2717 9.25 10.766L9.608 12.434C9.669 12.72 9.902 12.75 10 12.75C10.098 12.75 10.331 12.72 10.392 12.434L10.75 10.767C10.8565 10.2725 11.1033 9.81928 11.4609 9.46154C11.8185 9.10379 12.2716 8.85674 12.766 8.75L14.433 8.393C14.719 8.331 14.75 8.098 14.75 8C14.75 7.902 14.719 7.669 14.433 7.607L12.766 7.25C12.2717 7.14333 11.8187 6.89643 11.4611 6.53888C11.1036 6.18132 10.8567 5.72828 10.75 5.234L10.392 3.566C10.331 3.28 10.098 3.25 10 3.25Z" fill="context-fill"/>
    
    3
    +  <path d="M2.44399 12.151L2.62599 11.302C2.71199 10.9 3.28599 10.9 3.37299 11.302L3.55499 12.151C3.57035 12.2229 3.60618 12.2888 3.65817 12.3408C3.71016 12.3928 3.77609 12.4286 3.84799 12.444L4.69699 12.626C5.09899 12.712 5.09899 13.286 4.69699 13.373L3.84799 13.555C3.77609 13.5704 3.71016 13.6062 3.65817 13.6582C3.60618 13.7102 3.57035 13.7761 3.55499 13.848L3.37299 14.697C3.28699 15.099 2.71299 15.099 2.62599 14.697L2.44399 13.848C2.42863 13.7761 2.39279 13.7102 2.3408 13.6582C2.28881 13.6062 2.22289 13.5704 2.15099 13.555L1.30199 13.373C0.899988 13.287 0.899988 12.713 1.30199 12.626L2.15099 12.444C2.22294 12.4288 2.28893 12.393 2.34094 12.341C2.39295 12.2889 2.42875 12.223 2.44399 12.151Z" fill="context-fill"/>
    
    4
    +  <path d="M2.44399 2.151L2.62599 1.302C2.71199 0.900004 3.28599 0.900004 3.37299 1.302L3.55499 2.151C3.57035 2.22291 3.60618 2.28883 3.65817 2.34082C3.71016 2.39281 3.77609 2.42864 3.84799 2.444L4.69699 2.626C5.09899 2.712 5.09899 3.286 4.69699 3.373L3.84899 3.556C3.77703 3.57125 3.71105 3.60704 3.65904 3.65905C3.60703 3.71106 3.57123 3.77705 3.55599 3.849L3.37299 4.698C3.28699 5.1 2.71299 5.1 2.62599 4.698L2.44399 3.849C2.42875 3.77705 2.39295 3.71106 2.34094 3.65905C2.28893 3.60704 2.22294 3.57125 2.15099 3.556L1.30199 3.373C0.899988 3.287 0.899988 2.713 1.30199 2.626L2.15099 2.444C2.22294 2.42876 2.28893 2.39296 2.34094 2.34095C2.39295 2.28894 2.42875 2.22296 2.44399 2.151Z" fill="context-fill"/>
    
    5
    +</svg>

  • browser/components/torpreferences/content/provideBridgeDialog.js
    ... ... @@ -15,6 +15,46 @@ const { TorParsers } = ChromeUtils.importESModule(
    15 15
       "resource://gre/modules/TorParsers.sys.mjs"
    
    16 16
     );
    
    17 17
     
    
    18
    +const { Lox, LoxErrors } = ChromeUtils.importESModule(
    
    19
    +  "resource://gre/modules/Lox.sys.mjs"
    
    20
    +);
    
    21
    +
    
    22
    +/*
    
    23
    + * Fake Lox module:
    
    24
    +
    
    25
    +const LoxErrors = {
    
    26
    +  BadInvite: "BadInvite",
    
    27
    +  LoxServerUnreachable: "LoxServerUnreachable",
    
    28
    +  Other: "Other",
    
    29
    +};
    
    30
    +
    
    31
    +const Lox = {
    
    32
    +  failError: null,
    
    33
    +  // failError: LoxErrors.BadInvite,
    
    34
    +  // failError: LoxErrors.LoxServerUnreachable,
    
    35
    +  // failError: LoxErrors.Other,
    
    36
    +  redeemInvite(invite) {
    
    37
    +    return new Promise((res, rej) => {
    
    38
    +      setTimeout(() => {
    
    39
    +        if (this.failError) {
    
    40
    +          rej({ type: this.failError });
    
    41
    +        }
    
    42
    +        res("lox-id-000000");
    
    43
    +      }, 4000);
    
    44
    +    });
    
    45
    +  },
    
    46
    +  validateInvitation(invite) {
    
    47
    +    return invite.startsWith("lox-invite");
    
    48
    +  },
    
    49
    +  getBridges(id) {
    
    50
    +    return [
    
    51
    +      "0:0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    
    52
    +      "0:1 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
    
    53
    +    ];
    
    54
    +  },
    
    55
    +};
    
    56
    +*/
    
    57
    +
    
    18 58
     const gProvideBridgeDialog = {
    
    19 59
       init() {
    
    20 60
         this._result = window.arguments[0];
    
    ... ... @@ -36,10 +76,14 @@ const gProvideBridgeDialog = {
    36 76
     
    
    37 77
         document.l10n.setAttributes(document.documentElement, titleId);
    
    38 78
     
    
    79
    +    // TODO: Make conditional on Lox being enabled.
    
    80
    +    this._allowLoxInvite = mode !== "edit"; // && Lox.enabled
    
    81
    +
    
    39 82
         document.l10n.setAttributes(
    
    40 83
           document.getElementById("user-provide-bridge-textarea-label"),
    
    41
    -      // TODO change string when we can also accept Lox share codes.
    
    42
    -      "user-provide-bridge-dialog-textarea-addresses-label"
    
    84
    +      this._allowLoxInvite
    
    85
    +        ? "user-provide-bridge-dialog-textarea-addresses-or-invite-label"
    
    86
    +        : "user-provide-bridge-dialog-textarea-addresses-label"
    
    43 87
         );
    
    44 88
     
    
    45 89
         this._dialog = document.getElementById("user-provide-bridge-dialog");
    
    ... ... @@ -48,6 +92,9 @@ const gProvideBridgeDialog = {
    48 92
         this._errorEl = document.getElementById(
    
    49 93
           "user-provide-bridge-error-message"
    
    50 94
         );
    
    95
    +    this._connectingEl = document.getElementById(
    
    96
    +      "user-provide-bridge-connecting"
    
    97
    +    );
    
    51 98
         this._resultDescription = document.getElementById(
    
    52 99
           "user-provide-result-description"
    
    53 100
         );
    
    ... ... @@ -68,8 +115,9 @@ const gProvideBridgeDialog = {
    68 115
           // Set placeholder if not editing.
    
    69 116
           document.l10n.setAttributes(
    
    70 117
             this._textarea,
    
    71
    -        // TODO: change string when we can also accept Lox share codes.
    
    72
    -        "user-provide-bridge-dialog-textarea-addresses"
    
    118
    +        this._allowLoxInvite
    
    119
    +          ? "user-provide-bridge-dialog-textarea-addresses-or-invite"
    
    120
    +          : "user-provide-bridge-dialog-textarea-addresses"
    
    73 121
           );
    
    74 122
         }
    
    75 123
     
    
    ... ... @@ -98,7 +146,17 @@ const gProvideBridgeDialog = {
    98 146
         this._page = page;
    
    99 147
         this._dialog.classList.toggle("show-entry-page", page === "entry");
    
    100 148
         this._dialog.classList.toggle("show-result-page", page === "result");
    
    101
    -    if (page === "entry") {
    
    149
    +    this.takeFocus();
    
    150
    +    this.updateResult();
    
    151
    +    this.updateAcceptDisabled();
    
    152
    +    this.onAcceptStateChange();
    
    153
    +  },
    
    154
    +
    
    155
    +  /**
    
    156
    +   * Reset focus position in the dialog.
    
    157
    +   */
    
    158
    +  takeFocus() {
    
    159
    +    if (this._page === "entry") {
    
    102 160
           this._textarea.focus();
    
    103 161
         } else {
    
    104 162
           // Move focus to the <xul:window> element.
    
    ... ... @@ -106,9 +164,6 @@ const gProvideBridgeDialog = {
    106 164
           // button (with now different text).
    
    107 165
           document.documentElement.focus();
    
    108 166
         }
    
    109
    -
    
    110
    -    this.updateAcceptDisabled();
    
    111
    -    this.onAcceptStateChange();
    
    112 167
       },
    
    113 168
     
    
    114 169
       /**
    
    ... ... @@ -149,7 +204,36 @@ const gProvideBridgeDialog = {
    149 204
        */
    
    150 205
       updateAcceptDisabled() {
    
    151 206
         this._acceptButton.disabled =
    
    152
    -      this._page === "entry" && validateBridgeLines(this._textarea.value).empty;
    
    207
    +      this._page === "entry" && (this.isEmpty() || this._loxLoading);
    
    208
    +  },
    
    209
    +
    
    210
    +  /**
    
    211
    +   * The lox loading state.
    
    212
    +   *
    
    213
    +   * @type {boolean}
    
    214
    +   */
    
    215
    +  _loxLoading: false,
    
    216
    +
    
    217
    +  /**
    
    218
    +   * Set the lox loading state. I.e. whether we are connecting to the lox
    
    219
    +   * server.
    
    220
    +   *
    
    221
    +   * @param {boolean} isLoading - Whether we are loading or not.
    
    222
    +   */
    
    223
    +  setLoxLoading(isLoading) {
    
    224
    +    this._loxLoading = isLoading;
    
    225
    +    this._textarea.readOnly = isLoading;
    
    226
    +    this._connectingEl.classList.toggle("show-connecting", isLoading);
    
    227
    +    if (
    
    228
    +      isLoading &&
    
    229
    +      this._acceptButton.contains(
    
    230
    +        this._acceptButton.getRootNode().activeElement
    
    231
    +      )
    
    232
    +    ) {
    
    233
    +      // Move focus to the alert before we disable the button.
    
    234
    +      this._connectingEl.focus();
    
    235
    +    }
    
    236
    +    this.updateAcceptDisabled();
    
    153 237
       },
    
    154 238
     
    
    155 239
       /**
    
    ... ... @@ -166,23 +250,63 @@ const gProvideBridgeDialog = {
    166 250
         // Prevent closing the dialog.
    
    167 251
         event.preventDefault();
    
    168 252
     
    
    169
    -    const bridges = this.checkValue();
    
    170
    -    if (!bridges.length) {
    
    253
    +    if (this._loxLoading) {
    
    254
    +      // User can still click Next whilst loading.
    
    255
    +      console.error("Already have a pending lox invite");
    
    256
    +      return;
    
    257
    +    }
    
    258
    +
    
    259
    +    // Clear the result from any previous attempt.
    
    260
    +    delete this._result.loxId;
    
    261
    +    delete this._result.addresses;
    
    262
    +    // Clear any previous error.
    
    263
    +    this.updateError(null);
    
    264
    +
    
    265
    +    const value = this.checkValue();
    
    266
    +    if (!value) {
    
    267
    +      // Not valid.
    
    268
    +      return;
    
    269
    +    }
    
    270
    +    if (value.loxInvite) {
    
    271
    +      this.setLoxLoading(true);
    
    272
    +      Lox.redeemInvite(value.loxInvite)
    
    273
    +        .finally(() => {
    
    274
    +          // Set set the loading to false before setting the errors.
    
    275
    +          this.setLoxLoading(false);
    
    276
    +        })
    
    277
    +        .then(
    
    278
    +          loxId => {
    
    279
    +            this._result.loxId = loxId;
    
    280
    +            this.setPage("result");
    
    281
    +          },
    
    282
    +          loxError => {
    
    283
    +            console.error("Redeeming failed", loxError);
    
    284
    +            switch (loxError.type) {
    
    285
    +              case LoxErrors.BadInvite:
    
    286
    +                // TODO: distinguish between a bad invite, an invite that has
    
    287
    +                // expired, and an invite that has already been redeemed.
    
    288
    +                this.updateError({ type: "bad-invite" });
    
    289
    +                break;
    
    290
    +              case LoxErrors.LoxServerUnreachable:
    
    291
    +                this.updateError({ type: "no-server" });
    
    292
    +                break;
    
    293
    +              default:
    
    294
    +                this.updateError({ type: "invite-error" });
    
    295
    +                break;
    
    296
    +            }
    
    297
    +          }
    
    298
    +        );
    
    299
    +      return;
    
    300
    +    }
    
    301
    +
    
    302
    +    if (!value.addresses?.length) {
    
    171 303
           // Not valid
    
    172 304
           return;
    
    173 305
         }
    
    174
    -    this._result.bridges = bridges;
    
    175
    -    this.updateResult();
    
    306
    +    this._result.addresses = value.addresses;
    
    176 307
         this.setPage("result");
    
    177 308
       },
    
    178 309
     
    
    179
    -  /**
    
    180
    -   * The current timeout for updating the error.
    
    181
    -   *
    
    182
    -   * @type {integer?}
    
    183
    -   */
    
    184
    -  _updateErrorTimeout: null,
    
    185
    -
    
    186 310
       /**
    
    187 311
        * Update the displayed error.
    
    188 312
        *
    
    ... ... @@ -191,14 +315,13 @@ const gProvideBridgeDialog = {
    191 315
        */
    
    192 316
       updateError(error) {
    
    193 317
         // First clear the existing error.
    
    194
    -    if (this._updateErrorTimeout !== null) {
    
    195
    -      clearTimeout(this._updateErrorTimeout);
    
    196
    -    }
    
    197
    -    this._updateErrorTimeout = null;
    
    198 318
         this._errorEl.removeAttribute("data-l10n-id");
    
    199 319
         this._errorEl.textContent = "";
    
    200 320
         if (error) {
    
    201 321
           this._textarea.setAttribute("aria-invalid", "true");
    
    322
    +      // Move focus back to the text area, likely away from the Next button or
    
    323
    +      // the "Connecting..." alert.
    
    324
    +      this._textarea.focus();
    
    202 325
         } else {
    
    203 326
           this._textarea.removeAttribute("aria-invalid");
    
    204 327
         }
    
    ... ... @@ -216,54 +339,122 @@ const gProvideBridgeDialog = {
    216 339
             errorId = "user-provide-bridge-dialog-address-error";
    
    217 340
             errorArgs = { line: error.line };
    
    218 341
             break;
    
    342
    +      case "multiple-invites":
    
    343
    +        errorId = "user-provide-bridge-dialog-multiple-invites-error";
    
    344
    +        break;
    
    345
    +      case "mixed":
    
    346
    +        errorId = "user-provide-bridge-dialog-mixed-error";
    
    347
    +        break;
    
    348
    +      case "not-allowed-invite":
    
    349
    +        errorId = "user-provide-bridge-dialog-invite-not-allowed-error";
    
    350
    +        break;
    
    351
    +      case "bad-invite":
    
    352
    +        errorId = "user-provide-bridge-dialog-bad-invite-error";
    
    353
    +        break;
    
    354
    +      case "no-server":
    
    355
    +        errorId = "user-provide-bridge-dialog-no-server-error";
    
    356
    +        break;
    
    357
    +      case "invite-error":
    
    358
    +        // Generic invite error.
    
    359
    +        errorId = "user-provide-bridge-dialog-generic-invite-error";
    
    360
    +        break;
    
    219 361
         }
    
    220 362
     
    
    221
    -    // Wait a small amount of time to actually set the textContent. Otherwise
    
    222
    -    // the screen reader (tested with Orca) may not pick up on the change in
    
    223
    -    // text.
    
    224
    -    this._updateErrorTimeout = setTimeout(() => {
    
    225
    -      document.l10n.setAttributes(this._errorEl, errorId, errorArgs);
    
    226
    -    }, 500);
    
    363
    +    document.l10n.setAttributes(this._errorEl, errorId, errorArgs);
    
    364
    +  },
    
    365
    +
    
    366
    +  /**
    
    367
    +   * The condition for the value to be empty.
    
    368
    +   *
    
    369
    +   * @type {RegExp}
    
    370
    +   */
    
    371
    +  _emptyRegex: /^\s*$/,
    
    372
    +  /**
    
    373
    +   * Whether the input is considered empty.
    
    374
    +   *
    
    375
    +   * @returns {boolean} true if it is considered empty.
    
    376
    +   */
    
    377
    +  isEmpty() {
    
    378
    +    return this._emptyRegex.test(this._textarea.value);
    
    227 379
       },
    
    228 380
     
    
    229 381
       /**
    
    230 382
        * Check the current value in the textarea.
    
    231 383
        *
    
    232
    -   * @returns {string[]} - The bridge addresses, if the entry is valid.
    
    384
    +   * @returns {object?} - The bridge addresses, or lox invite, or null if no
    
    385
    +   *   valid value.
    
    233 386
        */
    
    234 387
       checkValue() {
    
    235
    -    let bridges = [];
    
    236
    -    let error = null;
    
    237
    -    const validation = validateBridgeLines(this._textarea.value);
    
    238
    -    if (!validation.empty) {
    
    388
    +    if (this.isEmpty()) {
    
    239 389
           // If empty, we just disable the button, rather than show an error.
    
    240
    -      if (validation.errorLines.length) {
    
    241
    -        // Report first error.
    
    242
    -        error = {
    
    243
    -          type: "invalid-address",
    
    244
    -          line: validation.errorLines[0],
    
    245
    -        };
    
    246
    -      } else {
    
    247
    -        bridges = validation.validBridges;
    
    390
    +      this.updateError(null);
    
    391
    +      return null;
    
    392
    +    }
    
    393
    +
    
    394
    +    let loxInvite = null;
    
    395
    +    for (let line of this._textarea.value.split(/\r?\n/)) {
    
    396
    +      line = line.trim();
    
    397
    +      if (!line) {
    
    398
    +        continue;
    
    399
    +      }
    
    400
    +      // TODO: Once we have a Lox invite encoding, distinguish between a valid
    
    401
    +      // invite and something that looks like it should be an invite.
    
    402
    +      const isLoxInvite = Lox.validateInvitation(line);
    
    403
    +      if (isLoxInvite) {
    
    404
    +        if (!this._allowLoxInvite) {
    
    405
    +          this.updateError({ type: "not-allowed-invite" });
    
    406
    +          return null;
    
    407
    +        }
    
    408
    +        if (loxInvite) {
    
    409
    +          this.updateError({ type: "multiple-invites" });
    
    410
    +          return null;
    
    411
    +        }
    
    412
    +        loxInvite = line;
    
    413
    +      } else if (loxInvite) {
    
    414
    +        this.updateError({ type: "mixed" });
    
    415
    +        return null;
    
    248 416
           }
    
    249 417
         }
    
    250
    -    this.updateError(error);
    
    251
    -    return bridges;
    
    418
    +
    
    419
    +    if (loxInvite) {
    
    420
    +      return { loxInvite };
    
    421
    +    }
    
    422
    +
    
    423
    +    const validation = validateBridgeLines(this._textarea.value);
    
    424
    +    if (validation.errorLines.length) {
    
    425
    +      // Report first error.
    
    426
    +      this.updateError({
    
    427
    +        type: "invalid-address",
    
    428
    +        line: validation.errorLines[0],
    
    429
    +      });
    
    430
    +      return null;
    
    431
    +    }
    
    432
    +
    
    433
    +    return { addresses: validation.validBridges };
    
    252 434
       },
    
    253 435
     
    
    254 436
       /**
    
    255 437
        * Update the shown result on the last page.
    
    256 438
        */
    
    257 439
       updateResult() {
    
    440
    +    if (this._page !== "result") {
    
    441
    +      return;
    
    442
    +    }
    
    443
    +
    
    444
    +    const loxId = this._result.loxId;
    
    445
    +
    
    258 446
         document.l10n.setAttributes(
    
    259 447
           this._resultDescription,
    
    260
    -      // TODO: Use a different id when added through Lox invite.
    
    261
    -      "user-provide-bridge-dialog-result-addresses"
    
    448
    +      loxId
    
    449
    +        ? "user-provide-bridge-dialog-result-invite"
    
    450
    +        : "user-provide-bridge-dialog-result-addresses"
    
    262 451
         );
    
    263 452
     
    
    264 453
         this._bridgeGrid.replaceChildren();
    
    265 454
     
    
    266
    -    for (const bridgeLine of this._result.bridges) {
    
    455
    +    const bridgeResult = loxId ? Lox.getBridges(loxId) : this._result.addresses;
    
    456
    +
    
    457
    +    for (const bridgeLine of bridgeResult) {
    
    267 458
           let details;
    
    268 459
           try {
    
    269 460
             details = TorParsers.parseBridgeLine(bridgeLine);
    
    ... ... @@ -305,6 +496,11 @@ const gProvideBridgeDialog = {
    305 496
       },
    
    306 497
     };
    
    307 498
     
    
    499
    +document.subDialogSetDefaultFocus = () => {
    
    500
    +  // Set the focus to the text area on load.
    
    501
    +  gProvideBridgeDialog.takeFocus();
    
    502
    +};
    
    503
    +
    
    308 504
     window.addEventListener(
    
    309 505
       "DOMContentLoaded",
    
    310 506
       () => {
    

  • browser/components/torpreferences/content/provideBridgeDialog.xhtml
    ... ... @@ -48,8 +48,15 @@
    48 48
           <html:div id="user-provide-bridge-message-area">
    
    49 49
             <html:span
    
    50 50
               id="user-provide-bridge-error-message"
    
    51
    +          role="alert"
    
    51 52
               aria-live="assertive"
    
    52 53
             ></html:span>
    
    54
    +        <html:span
    
    55
    +          id="user-provide-bridge-connecting"
    
    56
    +          role="alert"
    
    57
    +          tabindex="0"
    
    58
    +          data-l10n-id="user-provide-bridge-dialog-connecting"
    
    59
    +        ></html:span>
    
    53 60
           </html:div>
    
    54 61
         </html:div>
    
    55 62
         <html:div id="user-provide-bridge-result-page">
    

  • browser/components/torpreferences/content/torPreferences.css
    ... ... @@ -161,6 +161,10 @@
    161 161
       display: none;
    
    162 162
     }
    
    163 163
     
    
    164
    +#tor-bridges-current:not(.source-lox) #tor-bridges-lox-label {
    
    165
    +  display: none;
    
    166
    +}
    
    167
    +
    
    164 168
     #tor-bridges-current:not(
    
    165 169
       .source-user,
    
    166 170
       .source-requested
    
    ... ... @@ -168,6 +172,10 @@
    168 172
       display: none;
    
    169 173
     }
    
    170 174
     
    
    175
    +#tor-bridges-current:not(.source-lox) #tor-bridges-lox-status {
    
    176
    +  display: none;
    
    177
    +}
    
    178
    +
    
    171 179
     #tor-bridges-none,
    
    172 180
     #tor-bridges-current {
    
    173 181
       margin-inline: 0;
    
    ... ... @@ -214,6 +222,24 @@
    214 222
       flex: 0 0 auto;
    
    215 223
     }
    
    216 224
     
    
    225
    +#tor-bridges-lox-label {
    
    226
    +  display: flex;
    
    227
    +  align-items: center;
    
    228
    +  gap: 6px;
    
    229
    +}
    
    230
    +
    
    231
    +#tor-bridges-lox-label > * {
    
    232
    +  flex: 0 0 auto;
    
    233
    +}
    
    234
    +
    
    235
    +#tor-bridges-lox-label-icon {
    
    236
    +  content: url("chrome://browser/content/torpreferences/lox-bridge-pass.svg");
    
    237
    +  width: 16px;
    
    238
    +  height: 16px;
    
    239
    +  -moz-context-properties: fill;
    
    240
    +  fill: var(--in-content-icon-color);
    
    241
    +}
    
    242
    +
    
    217 243
     #tor-bridges-current-heading {
    
    218 244
       margin: 0;
    
    219 245
       margin-inline-end: 2em;
    
    ... ... @@ -397,11 +423,14 @@
    397 423
       clip-path: inset(50%);
    
    398 424
     }
    
    399 425
     
    
    400
    -#tor-bridges-share {
    
    426
    +.tor-bridges-details-box {
    
    401 427
       margin-block-start: 24px;
    
    402 428
       border-radius: 4px;
    
    403 429
       border: 1px solid var(--in-content-border-color);
    
    404 430
       padding: 16px;
    
    431
    +}
    
    432
    +
    
    433
    +#tor-bridges-share {
    
    405 434
       display: grid;
    
    406 435
       grid-template:
    
    407 436
         "heading heading heading" min-content
    
    ... ... @@ -452,6 +481,169 @@
    452 481
       fill: currentColor;
    
    453 482
     }
    
    454 483
     
    
    484
    +#tor-bridges-lox-status {
    
    485
    +  margin-block-start: 8px;
    
    486
    +}
    
    487
    +
    
    488
    +#tor-bridges-lox-status:not(.show-next-unlock) #tor-bridges-lox-details {
    
    489
    +  display: none;
    
    490
    +}
    
    491
    +
    
    492
    +#tor-bridges-lox-status:not(.show-unlock-alert) #tor-bridges-lox-unlock-alert {
    
    493
    +  display: none;
    
    494
    +}
    
    495
    +
    
    496
    +.tor-bridges-lox-box {
    
    497
    +  display: grid;
    
    498
    +  grid-template:
    
    499
    +    "image intro intro" min-content
    
    500
    +    ". list list" auto
    
    501
    +    ". invites button" min-content
    
    502
    +    / min-content 1fr max-content;
    
    503
    +  align-items: start;
    
    504
    +  gap: 0 8px;
    
    505
    +}
    
    506
    +
    
    507
    +.tor-bridges-lox-image-outer {
    
    508
    +  grid-area: image;
    
    509
    +  /* The ring is 36px by 36px, but has 4px of padding for a Gaussian blur. */
    
    510
    +  width: 44px;
    
    511
    +  height: 44px;
    
    512
    +  margin: -4px;
    
    513
    +  align-self: center;
    
    514
    +  justify-self: center;
    
    515
    +  /* fill is the progress, stroke is the empty ring. */
    
    516
    +  -moz-context-properties: fill, stroke;
    
    517
    +  fill: var(--in-content-success-icon-color);
    
    518
    +  stroke: var(--in-content-border-color);
    
    519
    +  content: url("chrome://browser/content/torpreferences/lox-progress-ring.svg");
    
    520
    +}
    
    521
    +
    
    522
    +#tor-bridges-lox-unlock-alert.lox-unlock-upgrade .tor-bridges-lox-image-outer {
    
    523
    +  content: url("chrome://browser/content/torpreferences/lox-complete-ring.svg");
    
    524
    +}
    
    525
    +
    
    526
    +.tor-bridges-lox-image-inner {
    
    527
    +  grid-area: image;
    
    528
    +  /* Extra 4px space for gaussian blur. */
    
    529
    +  width: 16px;
    
    530
    +  height: 16px;
    
    531
    +  align-self: center;
    
    532
    +  justify-self: center;
    
    533
    +  -moz-context-properties: fill;
    
    534
    +  fill: var(--in-content-icon-color);
    
    535
    +}
    
    536
    +
    
    537
    +#tor-bridges-lox-details .tor-bridges-lox-image-inner {
    
    538
    +  content: url("chrome://browser/content/torpreferences/lox-bridge-pass.svg");
    
    539
    +}
    
    540
    +
    
    541
    +#tor-bridges-lox-unlock-alert .tor-bridges-lox-image-inner {
    
    542
    +  content: url("chrome://browser/content/torpreferences/bridge.svg");
    
    543
    +}
    
    544
    +
    
    545
    +#tor-bridges-lox-unlock-alert.lox-unlock-upgrade .tor-bridges-lox-image-inner {
    
    546
    +  content: url("chrome://browser/content/torpreferences/lox-success.svg");
    
    547
    +}
    
    548
    +
    
    549
    +.tor-bridges-lox-intro {
    
    550
    +  grid-area: intro;
    
    551
    +  font-weight: 700;
    
    552
    +  align-self: center;
    
    553
    +}
    
    554
    +
    
    555
    +.tor-bridges-lox-list {
    
    556
    +  grid-area: list;
    
    557
    +  margin: 0;
    
    558
    +  padding: 0;
    
    559
    +  display: grid;
    
    560
    +  /* Align the icons, as if list markers. */
    
    561
    +  grid-template-columns: max-content 1fr;
    
    562
    +  align-items: start;
    
    563
    +}
    
    564
    +
    
    565
    +.tor-bridges-lox-list-item {
    
    566
    +  display: contents;
    
    567
    +}
    
    568
    +
    
    569
    +.tor-bridges-lox-list-item::before {
    
    570
    +  /* We use ::before rather than list-style-image to have more control. */
    
    571
    +  display: block;
    
    572
    +  box-sizing: content-box;
    
    573
    +  width: 18px;
    
    574
    +  height: 18px;
    
    575
    +  margin-inline: 4px 6px;
    
    576
    +  /* We want the icons to be center-aligned relative to the *first* line. */
    
    577
    +  /* TODO: After firefox 120, can use line-height unit "lh" to do proper
    
    578
    +   * center-alignment: calc((1lh - 18px) / 2)
    
    579
    +   * For now, we use 3.4ex as an approximation for 1lh */
    
    580
    +  margin-block-start: calc((3.4ex - 18px) / 2);
    
    581
    +  /* fill is the icon color, stroke is the border color. */
    
    582
    +  -moz-context-properties: fill, stroke;
    
    583
    +  fill: var(--in-content-icon-color);
    
    584
    +}
    
    585
    +
    
    586
    +.tor-bridges-lox-list-item-bridge::before {
    
    587
    +  content: url("chrome://browser/content/torpreferences/lox-bridge-icon.svg");
    
    588
    +}
    
    589
    +
    
    590
    +.tor-bridges-lox-list-item-invite::before {
    
    591
    +  content: url("chrome://browser/content/torpreferences/lox-invite-icon.svg");
    
    592
    +}
    
    593
    +
    
    594
    +#tor-bridges-lox-details .tor-bridges-lox-list-item::before {
    
    595
    +  stroke: var(--in-content-border-color);
    
    596
    +}
    
    597
    +
    
    598
    +#tor-bridges-lox-unlock-alert .tor-bridges-lox-list-item::before {
    
    599
    +  stroke: var(--in-content-success-icon-color);
    
    600
    +}
    
    601
    +
    
    602
    +#tor-bridges-lox-details:not(.lox-next-gain-bridges) #tor-bridges-lox-next-unlock-gain-bridges {
    
    603
    +  display: none;
    
    604
    +}
    
    605
    +
    
    606
    +#tor-bridges-lox-details:not(.lox-next-first-invites) #tor-bridges-lox-next-unlock-first-invites {
    
    607
    +  display: none;
    
    608
    +}
    
    609
    +
    
    610
    +#tor-bridges-lox-details:not(.lox-next-more-invites) #tor-bridges-lox-next-unlock-more-invites {
    
    611
    +  display: none;
    
    612
    +}
    
    613
    +
    
    614
    +
    
    615
    +#tor-bridges-lox-unlock-alert:not(.lox-unlock-gain-bridges) #tor-bridges-lox-unlock-alert-gain-bridges {
    
    616
    +  display: none;
    
    617
    +}
    
    618
    +
    
    619
    +#tor-bridges-lox-unlock-alert:not(.lox-unlock-new-bridges) #tor-bridges-lox-unlock-alert-new-bridges {
    
    620
    +  display: none;
    
    621
    +}
    
    622
    +
    
    623
    +#tor-bridges-lox-unlock-alert:not(.lox-unlock-invites) #tor-bridges-lox-unlock-alert-invites {
    
    624
    +  display: none;
    
    625
    +}
    
    626
    +
    
    627
    +#tor-bridges-lox-remaining-invites {
    
    628
    +  grid-area: invites;
    
    629
    +  justify-self: end;
    
    630
    +  align-self: center;
    
    631
    +}
    
    632
    +
    
    633
    +#tor-bridges-lox-details:not(.lox-has-invites) :is(
    
    634
    +  #tor-bridges-lox-remaining-invites,
    
    635
    +  #tor-bridges-lox-show-invites-button
    
    636
    +) {
    
    637
    +  display: none;
    
    638
    +}
    
    639
    +
    
    640
    +.tor-bridges-lox-button {
    
    641
    +  grid-area: button;
    
    642
    +  margin: 0;
    
    643
    +  line-height: 1;
    
    644
    +  align-self: center;
    
    645
    +}
    
    646
    +
    
    455 647
     #tor-bridges-provider-heading {
    
    456 648
       font-size: 1.14em;
    
    457 649
       margin-block: 48px 8px;
    
    ... ... @@ -723,6 +915,15 @@ groupbox#torPreferences-bridges-group textarea {
    723 915
       display: none;
    
    724 916
     }
    
    725 917
     
    
    918
    +#user-provide-bridge-connecting {
    
    919
    +  color: var(--text-color-deemphasized);
    
    920
    +  /* TODO: Add spinner ::before */
    
    921
    +}
    
    922
    +
    
    923
    +#user-provide-bridge-connecting:not(.show-connecting) {
    
    924
    +  display: none;
    
    925
    +}
    
    926
    +
    
    726 927
     #user-provide-bridge-result-page {
    
    727 928
       flex: 1 1 0;
    
    728 929
       min-height: 0;
    

  • browser/components/torpreferences/jar.mn
    ... ... @@ -3,6 +3,12 @@ browser.jar:
    3 3
         content/browser/torpreferences/bridge-qr.svg                     (content/bridge-qr.svg)
    
    4 4
         content/browser/torpreferences/telegram-logo.svg                 (content/telegram-logo.svg)
    
    5 5
         content/browser/torpreferences/bridge-bot.svg                    (content/bridge-bot.svg)
    
    6
    +    content/browser/torpreferences/lox-invite-icon.svg               (content/lox-invite-icon.svg)
    
    7
    +    content/browser/torpreferences/lox-bridge-icon.svg               (content/lox-bridge-icon.svg)
    
    8
    +    content/browser/torpreferences/lox-bridge-pass.svg               (content/lox-bridge-pass.svg)
    
    9
    +    content/browser/torpreferences/lox-success.svg                   (content/lox-success.svg)
    
    10
    +    content/browser/torpreferences/lox-complete-ring.svg             (content/lox-complete-ring.svg)
    
    11
    +    content/browser/torpreferences/lox-progress-ring.svg             (content/lox-progress-ring.svg)
    
    6 12
         content/browser/torpreferences/bridgeQrDialog.xhtml              (content/bridgeQrDialog.xhtml)
    
    7 13
         content/browser/torpreferences/bridgeQrDialog.js                 (content/bridgeQrDialog.js)
    
    8 14
         content/browser/torpreferences/builtinBridgeDialog.xhtml         (content/builtinBridgeDialog.xhtml)
    

  • browser/locales/en-US/browser/tor-browser.ftl
    ... ... @@ -56,6 +56,10 @@ tor-bridges-your-bridges = Your bridges
    56 56
     tor-bridges-source-user = Added by you
    
    57 57
     tor-bridges-source-built-in = Built-in
    
    58 58
     tor-bridges-source-requested = Requested from Tor
    
    59
    +# Here "Bridge pass" is a noun: a bridge pass gives users access to some tor bridges.
    
    60
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    61
    +# This is shown when the user is getting their bridges from Lox.
    
    62
    +tor-bridges-source-lox = Bridge pass
    
    59 63
     # The "..." menu button for all current bridges.
    
    60 64
     tor-bridges-options-button =
    
    61 65
         .title = All bridges
    
    ... ... @@ -116,12 +120,75 @@ tor-bridges-update-changed-bridges = Your Tor bridges have changed.
    116 120
     
    
    117 121
     # Shown for requested bridges and bridges added by the user.
    
    118 122
     tor-bridges-share-heading = Help others connect
    
    119
    -#
    
    120 123
     tor-bridges-share-description = Share your bridges with trusted contacts.
    
    121 124
     tor-bridges-copy-addresses-button = Copy addresses
    
    122 125
     tor-bridges-qr-addresses-button =
    
    123 126
         .title = Show QR code
    
    124 127
     
    
    128
    +# Shown when using a "bridge pass", i.e. using Lox.
    
    129
    +# Here "bridge pass" is a noun: a bridge pass gives users access to some tor bridges.
    
    130
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    131
    +# Here "bridge bot" refers to a service that automatically gives out bridges for the user to use, i.e. the Lox authority.
    
    132
    +tor-bridges-lox-description = With a bridge pass, the bridge bot will send you new bridges when your bridges get blocked. If your bridges don’t get blocked, you’ll unlock invites that let you share bridges with trusted contacts.
    
    133
    +# The number of days until the user's "bridge pass" is upgraded.
    
    134
    +# $numDays (Number) - The number of days until the next upgrade, an integer (1 or higher).
    
    135
    +# The "[one]" and "[other]" are special Fluent syntax to mark plural categories that depend on the value of "$numDays". You can use any number of plural categories that work for your locale: "[zero]", "[one]", "[two]", "[few]", "[many]" and/or "[other]". The "*" marks a category as default, and is required.
    
    136
    +# See https://projectfluent.org/fluent/guide/selectors.html .
    
    137
    +# So in English, the first form will be used if $numDays is "1" (singular) and the second form will be used if $numDays is anything else (plural).
    
    138
    +tor-bridges-lox-days-until-unlock =
    
    139
    +  { $numDays ->
    
    140
    +     [one] { $numDays } day until you unlock:
    
    141
    +    *[other] { $numDays } days until you unlock:
    
    142
    +  }
    
    143
    +# This is shown as a list item after "N days until you unlock:" when the user will gain two more bridges in the future.
    
    144
    +# Here "bridge bot" refers to a service that automatically gives out bridges for the user to use, i.e. the Lox authority.
    
    145
    +tor-bridges-lox-unlock-two-bridges = +2 bridges from the bridge bot
    
    146
    +# This is shown as a list item after "N days until you unlock:" when the user will gain access to invites for the first time.
    
    147
    +# Here "invites" is a noun, short for "invitations".
    
    148
    +tor-bridges-lox-unlock-first-invites = Invites for your trusted contacts
    
    149
    +# This is shown as a list item after "N days until you unlock:" when the user already has invites.
    
    150
    +# Here "invites" is a noun, short for "invitations".
    
    151
    +tor-bridges-lox-unlock-more-invites = More invites for your trusted contacts
    
    152
    +# Here "invite" is a noun, short for "invitation".
    
    153
    +# $numInvites (Number) - The number of invites remaining, an integer (0 or higher).
    
    154
    +# The "[one]" and "[other]" are special Fluent syntax to mark plural categories that depend on the value of "$numInvites". You can use any number of plural categories that work for your locale: "[zero]", "[one]", "[two]", "[few]", "[many]" and/or "[other]". The "*" marks a category as default, and is required.
    
    155
    +# See https://projectfluent.org/fluent/guide/selectors.html .
    
    156
    +# So in English, the first form will be used if $numInvites is "1" (singular) and the second form will be used if $numInvites is anything else (plural).
    
    157
    +tor-bridges-lox-remaining-invites =
    
    158
    +  { $numInvites ->
    
    159
    +     [one] { $numInvites } invite remaining
    
    160
    +    *[other] { $numInvites } invites remaining
    
    161
    +  }
    
    162
    +# Here "invites" is a noun, short for "invitations".
    
    163
    +tor-bridges-lox-show-invites-button = Show invites
    
    164
    +
    
    165
    +# Shown when the user's "bridge pass" has been upgraded.
    
    166
    +# Here "bridge pass" is a noun: a bridge pass gives users access to some tor bridges.
    
    167
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    168
    +tor-bridges-lox-upgrade = Your bridge pass has been upgraded!
    
    169
    +# Shown when the user's bridges accessed through "bridge pass" have been blocked.
    
    170
    +tor-bridges-lox-blocked = Your blocked bridges have been replaced
    
    171
    +# Shown *after* the user has had their blocked bridges replaced.
    
    172
    +# Here "bridge bot" refers to a service that automatically gives out bridges for the user to use, i.e. the Lox authority.
    
    173
    +tor-bridges-lox-new-bridges = New bridges from the bridge bot
    
    174
    +# Shown *after* the user has gained two more bridges.
    
    175
    +# Here "bridge bot" refers to a service that automatically gives out bridges for the user to use, i.e. the Lox authority.
    
    176
    +tor-bridges-lox-gained-two-bridges = +2 bridges from the bridge bot
    
    177
    +# Shown *after* a user's "bridge pass" has changed.
    
    178
    +# Here "invite" is a noun, short for "invitation".
    
    179
    +# $numInvites (Number) - The number of invites remaining, an integer (0 or higher).
    
    180
    +# The "[one]" and "[other]" are special Fluent syntax to mark plural categories that depend on the value of "$numInvites". You can use any number of plural categories that work for your locale: "[zero]", "[one]", "[two]", "[few]", "[many]" and/or "[other]". The "*" marks a category as default, and is required.
    
    181
    +# See https://projectfluent.org/fluent/guide/selectors.html .
    
    182
    +# So in English, the first form will be used if $numInvites is "1" (singular) and the second form will be used if $numInvites is anything else (plural).
    
    183
    +tor-bridges-lox-new-invites =
    
    184
    +  { $numInvites ->
    
    185
    +     [one] You now have { $numInvites } remaining invite for your trusted contacts
    
    186
    +    *[other] You now have { $numInvites } remaining invites for your trusted contacts
    
    187
    +  }
    
    188
    +# Button for the user to acknowledge a change in their "bridge pass".
    
    189
    +tor-bridges-lox-got-it-button = Got it
    
    190
    +
    
    191
    +
    
    125 192
     # Shown as a heading when the user has no current bridges.
    
    126 193
     tor-bridges-add-bridges-heading = Add bridges
    
    127 194
     # Shown as a heading when the user has existing bridges that can be replaced.
    
    ... ... @@ -182,13 +249,50 @@ user-provide-bridge-dialog-description = Use bridges provided by a trusted organ
    182 249
     user-provide-bridge-dialog-learn-more = Learn more
    
    183 250
     # Short accessible name for the bridge addresses text area.
    
    184 251
     user-provide-bridge-dialog-textarea-addresses-label = Bridge addresses
    
    252
    +# Here "invite" is a noun, short for "invitation".
    
    253
    +# Short accessible name for text area when it can accept either bridge address or a single "bridge pass" invite.
    
    254
    +user-provide-bridge-dialog-textarea-addresses-or-invite-label = Bridge addresses or invite
    
    185 255
     # Placeholder shown when adding new bridge addresses.
    
    186 256
     user-provide-bridge-dialog-textarea-addresses =
    
    187 257
         .placeholder = Paste your bridge addresses here
    
    258
    +# Placeholder shown when the user can add new bridge addresses or a single "bridge pass" invite.
    
    259
    +# Here "bridge pass invite" is a noun: a bridge pass invite can be shared with other users to give them their own bridge pass, so they can get access to tor bridges.
    
    260
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    261
    +# And "invite" is simply short for "invitation".
    
    262
    +# NOTE: "invite" is singular, whilst "addresses" is plural.
    
    263
    +user-provide-bridge-dialog-textarea-addresses-or-invite =
    
    264
    +    .placeholder = Paste your bridge addresses or a bridge pass invite here
    
    188 265
     # Error shown when one of the address lines is invalid.
    
    189 266
     # $line (Number) - The line number for the invalid address.
    
    190 267
     user-provide-bridge-dialog-address-error = Incorrectly formatted bridge address on line { $line }.
    
    268
    +# Error shown when the user has entered more than one "bridge pass" invite.
    
    269
    +# Here "invite" is a noun, short for "invitation".
    
    270
    +user-provide-bridge-dialog-multiple-invites-error = Cannot include more than one invite.
    
    271
    +# Error shown when the user has mixed their invite with addresses.
    
    272
    +# Here "invite" is a noun, short for "invitation".
    
    273
    +user-provide-bridge-dialog-mixed-error = Cannot mix bridge addresses with an invite.
    
    274
    +# Error shown when the user has entered an invite when it is not supported.
    
    275
    +# Here "bridge pass invite" is a noun: a bridge pass invite can be shared with other users to give them their own bridge pass, so they can get access to tor bridges.
    
    276
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    277
    +# And "invite" is simply short for "invitation".
    
    278
    +user-provide-bridge-dialog-invite-not-allowed-error = Cannot include a bridge pass invite.
    
    279
    +# Error shown when the invite was not accepted by the server.
    
    280
    +user-provide-bridge-dialog-bad-invite-error = Invite was not accepted. Try a different one.
    
    281
    +# Error shown when the "bridge pass" server does not respond.
    
    282
    +# Here "bridge pass" is a noun: a bridge pass gives users access to some tor bridges.
    
    283
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    284
    +user-provide-bridge-dialog-no-server-error = Unable to connect to bridge pass server.
    
    285
    +# Generic error when an invite failed.
    
    286
    +# Here "invite" is a noun, short for "invitation".
    
    287
    +user-provide-bridge-dialog-generic-invite-error = Failed to redeem invite.
    
    288
    +
    
    289
    +# Here "bridge pass" is a noun: a bridge pass gives users access to some tor bridges.
    
    290
    +# So "pass" is referring to something that gives permission or access. Similar to "token", "permit" or "voucher", but for permanent use rather than one-time.
    
    291
    +user-provide-bridge-dialog-connecting = Connecting to bridge pass server…
    
    191 292
     
    
    293
    +# Shown after the user has entered a "bridge pass" invite.
    
    294
    +user-provide-bridge-dialog-result-invite = The following bridges were shared with you.
    
    295
    +# Shown after the user has entered bridge addresses.
    
    192 296
     user-provide-bridge-dialog-result-addresses = The following bridges were entered by you.
    
    193 297
     user-provide-bridge-dialog-next-button =
    
    194 298
         .label = Next

  • toolkit/components/lox/Lox.sys.mjs
    ... ... @@ -128,12 +128,12 @@ class LoxImpl {
    128 128
       }
    
    129 129
     
    
    130 130
       /**
    
    131
    -   * Formats and returns bridges from the stored Lox credential
    
    131
    +   * Formats and returns bridges from the stored Lox credential.
    
    132 132
        *
    
    133
    -   * @param {string} loxid The id string associated with a lox credential
    
    133
    +   * @param {string} loxid The id string associated with a lox credential.
    
    134 134
        *
    
    135 135
        * @returns {string[]} An array of formatted bridge lines. The array is empty
    
    136
    -   * if there are no bridges.
    
    136
    +   *   if there are no bridges.
    
    137 137
        */
    
    138 138
       getBridges(loxid) {
    
    139 139
         if (!this.#initialized) {
    
    ... ... @@ -250,6 +250,7 @@ class LoxImpl {
    250 250
           this.#pubKeyPromise = this.#makeRequest("pubkeys", [])
    
    251 251
             .then(pubKeys => {
    
    252 252
               this.#pubKeys = JSON.stringify(pubKeys);
    
    253
    +          this.#store();
    
    253 254
             })
    
    254 255
             .catch(() => {
    
    255 256
               // We always try to update, but if that doesn't work fall back to stored data
    
    ... ... @@ -266,6 +267,7 @@ class LoxImpl {
    266 267
           this.#encTablePromise = this.#makeRequest("reachability", [])
    
    267 268
             .then(encTable => {
    
    268 269
               this.#encTable = JSON.stringify(encTable);
    
    270
    +          this.#store();
    
    269 271
             })
    
    270 272
             .catch(() => {
    
    271 273
               // Try to update first, but if that doesn't work fall back to stored data
    
    ... ... @@ -283,6 +285,7 @@ class LoxImpl {
    283 285
           this.#constantsPromise = this.#makeRequest("constants", [])
    
    284 286
             .then(constants => {
    
    285 287
               this.#constants = JSON.stringify(constants);
    
    288
    +          this.#store();
    
    286 289
             })
    
    287 290
             .catch(() => {
    
    288 291
               if (!this.#constants) {
    
    ... ... @@ -389,18 +392,17 @@ class LoxImpl {
    389 392
       }
    
    390 393
     
    
    391 394
       /**
    
    392
    -   * Parses an input string to check if it is a valid Lox invitation
    
    395
    +   * Parses an input string to check if it is a valid Lox invitation.
    
    393 396
        *
    
    394
    -   * @param {string} invite A Lox invitation
    
    395
    -   * @returns {Promise<bool>} A promise that resolves to true if the value passed
    
    396
    -   * in was a Lox invitation and false if it is not
    
    397
    +   * @param {string} invite A Lox invitation.
    
    398
    +   * @returns {bool} Whether the value passed in was a Lox invitation.
    
    397 399
        */
    
    398
    -  async validateInvitation(invite) {
    
    400
    +  validateInvitation(invite) {
    
    399 401
         if (!this.#initialized) {
    
    400 402
           throw new LoxError(LoxErrors.NotInitialized);
    
    401 403
         }
    
    402 404
         try {
    
    403
    -      await lazy.invitation_is_trusted(invite);
    
    405
    +      lazy.invitation_is_trusted(invite);
    
    404 406
         } catch (err) {
    
    405 407
           console.log(err);
    
    406 408
           return false;
    
    ... ... @@ -420,22 +422,27 @@ class LoxImpl {
    420 422
       }
    
    421 423
     
    
    422 424
       /**
    
    423
    -   * Redeems a Lox invitation to obtain a credential and bridges
    
    425
    +   * Redeems a Lox invitation to obtain a credential and bridges.
    
    424 426
        *
    
    425
    -   * @param {string} invite A Lox invitation
    
    426
    -   * @returns {Promise<string>} A promise with the loxid of the associated credential on success
    
    427
    +   * @param {string} invite A Lox invitation.
    
    428
    +   * @returns {string} The loxid of the associated credential on success.
    
    427 429
        */
    
    428 430
       async redeemInvite(invite) {
    
    429 431
         if (!this.#initialized) {
    
    430 432
           throw new LoxError(LoxErrors.NotInitialized);
    
    431 433
         }
    
    432 434
         await this.#getPubKeys();
    
    433
    -    let request = await lazy.open_invite(invite.invite);
    
    435
    +    let request = await lazy.open_invite(JSON.parse(invite).invite);
    
    434 436
         let id = this.#genLoxId();
    
    435
    -    let response = await this.#makeRequest(
    
    436
    -      "openreq",
    
    437
    -      JSON.parse(request).request
    
    438
    -    );
    
    437
    +    let response;
    
    438
    +    try {
    
    439
    +      response = await this.#makeRequest(
    
    440
    +        "openreq",
    
    441
    +        JSON.parse(request).request
    
    442
    +      );
    
    443
    +    } catch {
    
    444
    +      throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    445
    +    }
    
    439 446
         console.log("openreq response: ", response);
    
    440 447
         if (response.hasOwnProperty("error")) {
    
    441 448
           throw new LoxError(LoxErrors.BadInvite);
    
    ... ... @@ -451,9 +458,9 @@ class LoxImpl {
    451 458
       }
    
    452 459
     
    
    453 460
       /**
    
    454
    -   * Get metadata on all invites historically generated by this credential
    
    461
    +   * Get metadata on all invites historically generated by this credential.
    
    455 462
        *
    
    456
    -   * @returns {object[]} A list of all historical invites
    
    463
    +   * @returns {string[]} A list of all historical invites.
    
    457 464
        */
    
    458 465
       getInvites() {
    
    459 466
         if (!this.#initialized) {
    
    ... ... @@ -463,12 +470,14 @@ class LoxImpl {
    463 470
       }
    
    464 471
     
    
    465 472
       /**
    
    466
    -   * Generates a new trusted Lox invitation that a user can pass to their contacts
    
    473
    +   * Generates a new trusted Lox invitation that a user can pass to their
    
    474
    +   * contacts.
    
    467 475
        *
    
    468
    -   * @returns {Promise<string>} A promise that resolves to a valid Lox invitation. The promise
    
    469
    -   *    will reject if:
    
    470
    -   *      - there is no saved Lox credential
    
    471
    -   *      - the saved credential does not have any invitations available
    
    476
    +   * Throws if:
    
    477
    +   *  - there is no saved Lox credential, or
    
    478
    +   *  - the saved credential does not have any invitations available.
    
    479
    +   *
    
    480
    +   * @returns {string} A valid Lox invitation.
    
    472 481
        */
    
    473 482
       async generateInvite() {
    
    474 483
         if (!this.#initialized) {
    
    ... ... @@ -514,11 +523,10 @@ class LoxImpl {
    514 523
       }
    
    515 524
     
    
    516 525
       /**
    
    517
    -   * Get the number of invites that a user has remaining
    
    526
    +   * Get the number of invites that a user has remaining.
    
    518 527
        *
    
    519
    -   * @returns {Promise<int>} A promise with the number of invites that can still be generated
    
    520
    -   *    by a user's credential. This promise will reject if:
    
    521
    -   *        - There is no credential
    
    528
    +   * @returns {int} The number of invites that can still be generated by a
    
    529
    +   *   user's credential.
    
    522 530
        */
    
    523 531
       getRemainingInviteCount() {
    
    524 532
         if (!this.#initialized) {
    
    ... ... @@ -691,11 +699,10 @@ class LoxImpl {
    691 699
        */
    
    692 700
     
    
    693 701
       /**
    
    694
    -   * Get a list of accumulated events
    
    702
    +   * Get a list of accumulated events.
    
    695 703
        *
    
    696
    -   * @returns {Promise<EventData[]>} A promise with a list of the accumulated,
    
    697
    -   *   unacknowledged events associated with a user's credential. This promise will reject if
    
    698
    -   *        - There is no credential
    
    704
    +   * @returns {EventData[]} A list of the accumulated, unacknowledged events
    
    705
    +   *   associated with a user's credential.
    
    699 706
        */
    
    700 707
       getEventData() {
    
    701 708
         if (!this.#initialized) {
    
    ... ... @@ -709,7 +716,7 @@ class LoxImpl {
    709 716
       }
    
    710 717
     
    
    711 718
       /**
    
    712
    -   * Clears accumulated event data
    
    719
    +   * Clears accumulated event data.
    
    713 720
        */
    
    714 721
       clearEventData() {
    
    715 722
         if (!this.#initialized) {
    
    ... ... @@ -720,7 +727,7 @@ class LoxImpl {
    720 727
       }
    
    721 728
     
    
    722 729
       /**
    
    723
    -   * Clears accumulated invitations
    
    730
    +   * Clears accumulated invitations.
    
    724 731
        */
    
    725 732
       clearInvites() {
    
    726 733
         if (!this.#initialized) {
    
    ... ... @@ -739,7 +746,9 @@ class LoxImpl {
    739 746
        */
    
    740 747
     
    
    741 748
       /**
    
    742
    -   * Get dates at which access to new features will unlock
    
    749
    +   * Get details about the next feature unlock.
    
    750
    +   *
    
    751
    +   * @returns {UnlockData} - Details about the next unlock.
    
    743 752
        */
    
    744 753
       async getNextUnlock() {
    
    745 754
         if (!this.#initialized) {
    
    ... ... @@ -753,10 +762,10 @@ class LoxImpl {
    753 762
         let nextUnlocks = JSON.parse(
    
    754 763
           lazy.get_next_unlock(this.#constants, this.#credentials[loxid])
    
    755 764
         );
    
    756
    -    const level = lazy.get_trust_level(this.#credentials[loxid]);
    
    765
    +    const level = parseInt(lazy.get_trust_level(this.#credentials[loxid]));
    
    757 766
         const unlocks = {
    
    758 767
           date: nextUnlocks.trust_level_unlock_date,
    
    759
    -      level: level + 1,
    
    768
    +      nextLevel: level + 1,
    
    760 769
         };
    
    761 770
         return unlocks;
    
    762 771
       }