<commit_before>// @flow
import { action, computed, observable } from 'mobx';
import { get } from 'lodash';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
import { ROUTES } from '../routes-config';
import {
  TransactionStates,
  WalletTransaction,
} from '../domains/WalletTransaction';
import { formattedArrayBufferToHexString } from '../utils/formatters';
import walletUtils from '../utils/walletUtils';
import {
  VOTING_CAST_START_DATE,
  VOTING_CAST_END_DATE,
  VOTING_RESULTS_DATE,
  VOTING_PHASE_CHECK_INTERVAL,
  VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL,
  VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS,
  NEXT_VOTING_FUND_NUMBER,
} from '../config/votingConfig';
import { votingPDFGenerator } from '../utils/votingPDFGenerator';
import { i18nContext } from '../utils/i18nContext';
import type { PathRoleIdentityType } from '../utils/hardwareWalletUtils';
import type {
  GetTransactionRequest,
  VotingMetadataType,
} from '../api/transactions/types';

export type VotingRegistrationKeyType = { bytes: Function, public: Function };

export type VotingDataType = {
  stakeAddress: string,
  stakeAddressHex: string,
  votingKey: string,
  stakeKey: string,
  role: PathRoleIdentityType,
  index: string,
  metadata: VotingMetadataType,
  absoluteSlotNumber: number,
};

export type FundPhase = 'snapshot' | 'voting' | 'tallying' | 'results';
export const FundPhases: EnumMap<string, FundPhase> = {
  SNAPSHOT: 'snapshot',
  VOTING: 'voting',
  TALLYING: 'tallying',
  RESULTS: 'results',
};

export default class VotingStore extends Store {
  @observable registrationStep: number = 1;
  @observable selectedWalletId: ?string = null;
  @observable transactionId: ?string = null;
  @observable transactionConfirmations: number = 0;
  @observable isTransactionPending: boolean = false;
  @observable isTransactionConfirmed: boolean = false;
  @observable votingRegistrationKey: ?VotingRegistrationKeyType = null;
  @observable qrCode: ?string = null;
  @observable isConfirmationDialogOpen: boolean = false;
  @observable fundPhase: FundPhase = FundPhases.SNAPSHOT;

  transactionPollingInterval: ?IntervalID = null;
  fundPhaseInterval: ?IntervalID = null;

  setup() {
    const { voting: votingActions } = this.actions;
    votingActions.selectWallet.listen(this._setSelectedWalletId);
    votingActions.sendTransaction.listen(this._sendTransaction);
    votingActions.generateQrCode.listen(this._generateQrCode);
    votingActions.saveAsPDF.listen(this._saveAsPDF);
    votingActions.nextRegistrationStep.listen(this._nextRegistrationStep);
    votingActions.previousRegistrationStep.listen(
      this._previousRegistrationStep
    );
    votingActions.resetRegistration.listen(this._resetRegistration);
    votingActions.showConfirmationDialog.listen(this._showConfirmationDialog);
    votingActions.closeConfirmationDialog.listen(this._closeConfirmationDialog);
    this._initializeFundPhaseInterval();
  }

  // REQUESTS

  @observable getWalletPublicKeyRequest: Request<string> = new Request(
    this.api.ada.getWalletPublicKey
  );

  @observable
  createVotingRegistrationTransactionRequest: Request<WalletTransaction> = new Request(
    this.api.ada.createVotingRegistrationTransaction
  );

  @observable
  signMetadataRequest: Request<Buffer> = new Request(
    this.api.ada.createWalletSignature
  );

  @observable
  getTransactionRequest: Request<GetTransactionRequest> = new Request(
    this.api.ada.getTransaction
  );

  // ACTIONS

  @action _showConfirmationDialog = () => {
    this.isConfirmationDialogOpen = true;
  };

  @action _closeConfirmationDialog = () => {
    this.isConfirmationDialogOpen = false;
  };

  @action _setSelectedWalletId = (walletId: string) => {
    this.selectedWalletId = walletId;
  };

  @action _nextRegistrationStep = () => {
    this.registrationStep++;
  };

  @action _previousRegistrationStep = () => {
    this.registrationStep--;
  };

  @action _resetRegistration = () => {
    this.isConfirmationDialogOpen = false;
    this.registrationStep = 1;
    this.selectedWalletId = null;
    this.transactionId = null;
    this.transactionConfirmations = 0;
    this.isTransactionPending = false;
    this.isTransactionConfirmed = false;
    this.votingRegistrationKey = null;
    this.qrCode = null;
    this.getWalletPublicKeyRequest.reset();
    this.createVotingRegistrationTransactionRequest.reset();
    this.signMetadataRequest.reset();
    if (this.transactionPollingInterval) {
      clearInterval(this.transactionPollingInterval);
    }
    if (this.fundPhaseInterval) {
      clearInterval(this.fundPhaseInterval);
    }
  };

  @action _startTransactionPolling = () => {
    if (this.transactionPollingInterval)
      clearInterval(this.transactionPollingInterval);
    this.transactionPollingInterval = setInterval(() => {
      this._checkVotingRegistrationTransaction();
    }, VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL);
  };

  @action _initializeFundPhaseInterval = () => {
    if (this.fundPhaseInterval) {
      clearInterval(this.fundPhaseInterval);
    }

    this.fundPhaseInterval = setInterval(() => {
      this._checkFundPhase(new Date());
    }, VOTING_PHASE_CHECK_INTERVAL);
  };

  @action _setVotingRegistrationKey = (value: VotingRegistrationKeyType) => {
    this.votingRegistrationKey = value;
  };

  @action _setTransactionId = (transactionId: string) => {
    this.transactionId = transactionId;
  };

  @action _setTransactionConfirmations = (confirmations: number) => {
    this.transactionConfirmations = confirmations;
  };

  @action _setIsTransactionPending = (value: boolean) => {
    this.isTransactionPending = value;
  };

  @action _setIsTransactionConfirmed = (value: boolean) => {
    this.isTransactionConfirmed = value;
  };

  @action _setQrCode = (value: ?string) => {
    this.qrCode = value;
  };

  prepareVotingData = async ({ walletId }: { walletId: string }) => {
    try {
      const { stakeAddresses } = this.stores.addresses;
      const stakeAddress = stakeAddresses[walletId];
      const stakeAddressHex = await this._getHexFromBech32(stakeAddress);

      await this._generateVotingRegistrationKey();
      if (!this.votingRegistrationKey)
        throw new Error('Failed to generate voting registration key.');
      const votingKey = formattedArrayBufferToHexString(
        this.votingRegistrationKey.public().bytes()
      );

      const stakeKeyBech32 = await this.getWalletPublicKeyRequest.execute({
        walletId,
        role: 'mutable_account',
        index: '0',
      });
      const stakeKey = await this._getHexFromBech32(stakeKeyBech32);
      const { absoluteSlotNumber } = this.stores.networkStatus;
      const metadata = {
        [61284]: {
          map: [
            {
              k: {
                int: 1,
              },
              v: {
                bytes: votingKey,
              },
            },
            {
              k: {
                int: 2,
              },
              v: {
                bytes: stakeKey,
              },
            },
            {
              k: {
                int: 3,
              },
              v: {
                bytes: stakeAddressHex,
              },
            },
            {
              k: {
                int: 4,
              },
              v: {
                int: absoluteSlotNumber,
              },
            },
          ],
        },
        [61285]: {
          map: [
            {
              k: {
                int: 1,
              },
              v: {
                bytes:
                  '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
              },
            },
          ],
        },
      };

      const votingData = {
        stakeAddress,
        stakeAddressHex,
        votingKey,
        stakeKey,
        role: 'mutable_account',
        index: '0',
        metadata,
        nonce: absoluteSlotNumber,
      };
      return votingData;
    } catch (e) {
      throw e;
    }
  };

  _sendTransaction = async ({
    amount,
    passphrase,
  }: {
    amount: number,
    passphrase: ?string,
  }) => {
    const walletId = this.selectedWalletId;
    if (!walletId)
      throw new Error(
        'Selected wallet required before send voting registration.'
      );
    const [address] = await this.stores.addresses.getAddressesByWalletId(
      walletId
    );
    const selectedWallet = this.stores.wallets.getWalletById(walletId);
    const isHardwareWallet = get(selectedWallet, 'isHardwareWallet', false);
    const { absoluteSlotNumber } = this.stores.networkStatus;

    // Reset voting registration transaction state
    this._setIsTransactionPending(true);
    this._setIsTransactionConfirmed(false);

    // Reset voting registration requests
    this.getWalletPublicKeyRequest.reset();
    this.createVotingRegistrationTransactionRequest.reset();
    this.signMetadataRequest.reset();

    let transaction;
    try {
      if (isHardwareWallet) {
        transaction = await this.stores.hardwareWallets._sendMoney({
          isVotingRegistrationTransaction: true,
          selectedWalletId: walletId,
        });
      } else {
        const votingData = await this.prepareVotingData({ walletId });
        const {
          stakeAddressHex,
          votingKey,
          stakeKey,
          role,
          index,
        } = votingData;

        const signature = await this.signMetadataRequest.execute({
          addressHex: stakeAddressHex,
          walletId,
          passphrase,
          votingKey,
          stakeKey,
          role,
          index,
          absoluteSlotNumber,
        });

        transaction = await this.createVotingRegistrationTransactionRequest.execute(
          {
            address: address.id,
            addressHex: stakeAddressHex,
            amount,
            passphrase,
            walletId,
            votingKey,
            stakeKey,
            signature: signature.toString('hex'),
            absoluteSlotNumber,
          }
        );
      }

      this._setTransactionId(transaction.id);
      if (!isHardwareWallet) {
        this._startTransactionPolling();
        this._nextRegistrationStep();
      }
    } catch (error) {
      if (error.code === 'wrong_encryption_passphrase') {
        // In case of a invalid spending password we stay on the same screen
        this._setIsTransactionPending(false);
      } else {
        // For any other error code we proceed to the next screen
        this._nextRegistrationStep();
      }
      throw error;
    }
  };

  _generateQrCode = async (pinCode: number) => {
    const { symmetric_encrypt: symmetricEncrypt } = await walletUtils;
    const password = new Uint8Array(4);
    pinCode
      .toString()
      .split('')
      .forEach((value: string, index: number) => {
        password[index] = parseInt(value, 10);
      });
    if (!this.votingRegistrationKey)
      throw new Error(
        'Failed to generate QR code due to missing voting registration key.'
      );
    const encrypt = symmetricEncrypt(
      password,
      this.votingRegistrationKey.bytes()
    );
    this._setQrCode(formattedArrayBufferToHexString(encrypt));
    this._nextRegistrationStep();
  };

  _saveAsPDF = async () => {
    const { qrCode, selectedWalletId } = this;
    if (!qrCode || !selectedWalletId) return;
    const selectedWallet = this.stores.wallets.getWalletById(selectedWalletId);
    if (!selectedWallet) return;
    const { name: walletName } = selectedWallet;
    const { desktopDirectoryPath } = this.stores.profile;
    const {
      currentLocale,
      currentDateFormat,
      currentTimeFormat,
    } = this.stores.profile;
    const nextVotingFundNumber = NEXT_VOTING_FUND_NUMBER;
    const { network, isMainnet } = this.environment;
    const intl = i18nContext(currentLocale);

    try {
      await votingPDFGenerator({
        nextVotingFundNumber,
        qrCode,
        walletName,
        currentLocale,
        currentDateFormat,
        currentTimeFormat,
        desktopDirectoryPath,
        network,
        isMainnet,
        intl,
      });
      this.actions.voting.saveAsPDFSuccess.trigger();
    } catch (error) {
      throw new Error(error);
    }
  };

  _checkVotingRegistrationTransaction = async () => {
    const {
      confirmations,
      state,
    }: WalletTransaction = await this.getTransactionRequest.execute({
      walletId: this.selectedWalletId,
      transactionId: this.transactionId,
    });

    // Update voting registration confirmations count
    if (this.transactionConfirmations !== confirmations) {
      this._setTransactionConfirmations(confirmations);
    }

    // Update voting registration pending state
    if (this.isTransactionPending && state === TransactionStates.OK) {
      this._setIsTransactionPending(false);
    }

    // Update voting registration confirmed state
    if (
      !this.isTransactionConfirmed &&
      confirmations >= VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS
    ) {
      this._setIsTransactionConfirmed(true);
      if (this.transactionPollingInterval)
        clearInterval(this.transactionPollingInterval);
    }
  };

  @action _checkFundPhase = (now: Date) => {
    const phaseValidation = {
      [FundPhases.SNAPSHOT]: now < VOTING_CAST_START_DATE,
      [FundPhases.VOTING]:
        now >= VOTING_CAST_START_DATE && now <= VOTING_CAST_END_DATE,
      [FundPhases.TALLYING]:
        now > VOTING_CAST_END_DATE && now < VOTING_RESULTS_DATE,
      [FundPhases.RESULTS]: now >= VOTING_RESULTS_DATE,
    };
    this.fundPhase =
      Object.keys(FundPhases)
        .map((phase) => FundPhases[phase])
        .find((phase) => phaseValidation[phase]) || FundPhases.SNAPSHOT;
  };

  _generateVotingRegistrationKey = async () => {
    const { Ed25519ExtendedPrivate: extendedPrivateKey } = await walletUtils;
    this._setVotingRegistrationKey(extendedPrivateKey.generate());
  };

  _getHexFromBech32 = async (key: string): Promise<string> => {
    const { bech32_decode_to_bytes: decodeBech32ToBytes } = await walletUtils;
    return formattedArrayBufferToHexString(decodeBech32ToBytes(key));
  };

  // GETTERS

  @computed get currentRoute(): string {
    return this.stores.router.location.pathname;
  }

  @computed get isVotingPage(): boolean {
    return this.currentRoute.indexOf(ROUTES.VOTING.REGISTRATION) > -1;
  }
}
<commit_msg>[DDW-796] Lazy validation<commit_after>// @flow
import { action, computed, observable } from 'mobx';
import { get } from 'lodash';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
import { ROUTES } from '../routes-config';
import {
  TransactionStates,
  WalletTransaction,
} from '../domains/WalletTransaction';
import { formattedArrayBufferToHexString } from '../utils/formatters';
import walletUtils from '../utils/walletUtils';
import {
  VOTING_CAST_START_DATE,
  VOTING_CAST_END_DATE,
  VOTING_RESULTS_DATE,
  VOTING_PHASE_CHECK_INTERVAL,
  VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL,
  VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS,
  NEXT_VOTING_FUND_NUMBER,
} from '../config/votingConfig';
import { votingPDFGenerator } from '../utils/votingPDFGenerator';
import { i18nContext } from '../utils/i18nContext';
import type { PathRoleIdentityType } from '../utils/hardwareWalletUtils';
import type {
  GetTransactionRequest,
  VotingMetadataType,
} from '../api/transactions/types';

export type VotingRegistrationKeyType = { bytes: Function, public: Function };

export type VotingDataType = {
  stakeAddress: string,
  stakeAddressHex: string,
  votingKey: string,
  stakeKey: string,
  role: PathRoleIdentityType,
  index: string,
  metadata: VotingMetadataType,
  absoluteSlotNumber: number,
};

export type FundPhase = 'snapshot' | 'voting' | 'tallying' | 'results';
export const FundPhases: EnumMap<string, FundPhase> = {
  SNAPSHOT: 'snapshot',
  VOTING: 'voting',
  TALLYING: 'tallying',
  RESULTS: 'results',
};

export default class VotingStore extends Store {
  @observable registrationStep: number = 1;
  @observable selectedWalletId: ?string = null;
  @observable transactionId: ?string = null;
  @observable transactionConfirmations: number = 0;
  @observable isTransactionPending: boolean = false;
  @observable isTransactionConfirmed: boolean = false;
  @observable votingRegistrationKey: ?VotingRegistrationKeyType = null;
  @observable qrCode: ?string = null;
  @observable isConfirmationDialogOpen: boolean = false;
  @observable fundPhase: FundPhase = FundPhases.SNAPSHOT;

  transactionPollingInterval: ?IntervalID = null;
  fundPhaseInterval: ?IntervalID = null;

  setup() {
    const { voting: votingActions } = this.actions;
    votingActions.selectWallet.listen(this._setSelectedWalletId);
    votingActions.sendTransaction.listen(this._sendTransaction);
    votingActions.generateQrCode.listen(this._generateQrCode);
    votingActions.saveAsPDF.listen(this._saveAsPDF);
    votingActions.nextRegistrationStep.listen(this._nextRegistrationStep);
    votingActions.previousRegistrationStep.listen(
      this._previousRegistrationStep
    );
    votingActions.resetRegistration.listen(this._resetRegistration);
    votingActions.showConfirmationDialog.listen(this._showConfirmationDialog);
    votingActions.closeConfirmationDialog.listen(this._closeConfirmationDialog);
    this._initializeFundPhaseInterval();
  }

  // REQUESTS

  @observable getWalletPublicKeyRequest: Request<string> = new Request(
    this.api.ada.getWalletPublicKey
  );

  @observable
  createVotingRegistrationTransactionRequest: Request<WalletTransaction> = new Request(
    this.api.ada.createVotingRegistrationTransaction
  );

  @observable
  signMetadataRequest: Request<Buffer> = new Request(
    this.api.ada.createWalletSignature
  );

  @observable
  getTransactionRequest: Request<GetTransactionRequest> = new Request(
    this.api.ada.getTransaction
  );

  // ACTIONS

  @action _showConfirmationDialog = () => {
    this.isConfirmationDialogOpen = true;
  };

  @action _closeConfirmationDialog = () => {
    this.isConfirmationDialogOpen = false;
  };

  @action _setSelectedWalletId = (walletId: string) => {
    this.selectedWalletId = walletId;
  };

  @action _nextRegistrationStep = () => {
    this.registrationStep++;
  };

  @action _previousRegistrationStep = () => {
    this.registrationStep--;
  };

  @action _resetRegistration = () => {
    this.isConfirmationDialogOpen = false;
    this.registrationStep = 1;
    this.selectedWalletId = null;
    this.transactionId = null;
    this.transactionConfirmations = 0;
    this.isTransactionPending = false;
    this.isTransactionConfirmed = false;
    this.votingRegistrationKey = null;
    this.qrCode = null;
    this.getWalletPublicKeyRequest.reset();
    this.createVotingRegistrationTransactionRequest.reset();
    this.signMetadataRequest.reset();
    if (this.transactionPollingInterval) {
      clearInterval(this.transactionPollingInterval);
    }
    if (this.fundPhaseInterval) {
      clearInterval(this.fundPhaseInterval);
    }
  };

  @action _startTransactionPolling = () => {
    if (this.transactionPollingInterval)
      clearInterval(this.transactionPollingInterval);
    this.transactionPollingInterval = setInterval(() => {
      this._checkVotingRegistrationTransaction();
    }, VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL);
  };

  @action _initializeFundPhaseInterval = () => {
    if (this.fundPhaseInterval) {
      clearInterval(this.fundPhaseInterval);
    }

    this.fundPhaseInterval = setInterval(() => {
      this._checkFundPhase(new Date());
    }, VOTING_PHASE_CHECK_INTERVAL);
  };

  @action _setVotingRegistrationKey = (value: VotingRegistrationKeyType) => {
    this.votingRegistrationKey = value;
  };

  @action _setTransactionId = (transactionId: string) => {
    this.transactionId = transactionId;
  };

  @action _setTransactionConfirmations = (confirmations: number) => {
    this.transactionConfirmations = confirmations;
  };

  @action _setIsTransactionPending = (value: boolean) => {
    this.isTransactionPending = value;
  };

  @action _setIsTransactionConfirmed = (value: boolean) => {
    this.isTransactionConfirmed = value;
  };

  @action _setQrCode = (value: ?string) => {
    this.qrCode = value;
  };

  prepareVotingData = async ({ walletId }: { walletId: string }) => {
    try {
      const { stakeAddresses } = this.stores.addresses;
      const stakeAddress = stakeAddresses[walletId];
      const stakeAddressHex = await this._getHexFromBech32(stakeAddress);

      await this._generateVotingRegistrationKey();
      if (!this.votingRegistrationKey)
        throw new Error('Failed to generate voting registration key.');
      const votingKey = formattedArrayBufferToHexString(
        this.votingRegistrationKey.public().bytes()
      );

      const stakeKeyBech32 = await this.getWalletPublicKeyRequest.execute({
        walletId,
        role: 'mutable_account',
        index: '0',
      });
      const stakeKey = await this._getHexFromBech32(stakeKeyBech32);
      const { absoluteSlotNumber } = this.stores.networkStatus;
      const metadata = {
        [61284]: {
          map: [
            {
              k: {
                int: 1,
              },
              v: {
                bytes: votingKey,
              },
            },
            {
              k: {
                int: 2,
              },
              v: {
                bytes: stakeKey,
              },
            },
            {
              k: {
                int: 3,
              },
              v: {
                bytes: stakeAddressHex,
              },
            },
            {
              k: {
                int: 4,
              },
              v: {
                int: absoluteSlotNumber,
              },
            },
          ],
        },
        [61285]: {
          map: [
            {
              k: {
                int: 1,
              },
              v: {
                bytes:
                  '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
              },
            },
          ],
        },
      };

      const votingData = {
        stakeAddress,
        stakeAddressHex,
        votingKey,
        stakeKey,
        role: 'mutable_account',
        index: '0',
        metadata,
        nonce: absoluteSlotNumber,
      };
      return votingData;
    } catch (e) {
      throw e;
    }
  };

  _sendTransaction = async ({
    amount,
    passphrase,
  }: {
    amount: number,
    passphrase: ?string,
  }) => {
    const walletId = this.selectedWalletId;
    if (!walletId)
      throw new Error(
        'Selected wallet required before send voting registration.'
      );
    const [address] = await this.stores.addresses.getAddressesByWalletId(
      walletId
    );
    const selectedWallet = this.stores.wallets.getWalletById(walletId);
    const isHardwareWallet = get(selectedWallet, 'isHardwareWallet', false);
    const { absoluteSlotNumber } = this.stores.networkStatus;

    // Reset voting registration transaction state
    this._setIsTransactionPending(true);
    this._setIsTransactionConfirmed(false);

    // Reset voting registration requests
    this.getWalletPublicKeyRequest.reset();
    this.createVotingRegistrationTransactionRequest.reset();
    this.signMetadataRequest.reset();

    let transaction;
    try {
      if (isHardwareWallet) {
        transaction = await this.stores.hardwareWallets._sendMoney({
          isVotingRegistrationTransaction: true,
          selectedWalletId: walletId,
        });
      } else {
        const votingData = await this.prepareVotingData({ walletId });
        const {
          stakeAddressHex,
          votingKey,
          stakeKey,
          role,
          index,
        } = votingData;

        const signature = await this.signMetadataRequest.execute({
          addressHex: stakeAddressHex,
          walletId,
          passphrase,
          votingKey,
          stakeKey,
          role,
          index,
          absoluteSlotNumber,
        });

        transaction = await this.createVotingRegistrationTransactionRequest.execute(
          {
            address: address.id,
            addressHex: stakeAddressHex,
            amount,
            passphrase,
            walletId,
            votingKey,
            stakeKey,
            signature: signature.toString('hex'),
            absoluteSlotNumber,
          }
        );
      }

      this._setTransactionId(transaction.id);
      if (!isHardwareWallet) {
        this._startTransactionPolling();
        this._nextRegistrationStep();
      }
    } catch (error) {
      if (error.code === 'wrong_encryption_passphrase') {
        // In case of a invalid spending password we stay on the same screen
        this._setIsTransactionPending(false);
      } else {
        // For any other error code we proceed to the next screen
        this._nextRegistrationStep();
      }
      throw error;
    }
  };

  _generateQrCode = async (pinCode: number) => {
    const { symmetric_encrypt: symmetricEncrypt } = await walletUtils;
    const password = new Uint8Array(4);
    pinCode
      .toString()
      .split('')
      .forEach((value: string, index: number) => {
        password[index] = parseInt(value, 10);
      });
    if (!this.votingRegistrationKey)
      throw new Error(
        'Failed to generate QR code due to missing voting registration key.'
      );
    const encrypt = symmetricEncrypt(
      password,
      this.votingRegistrationKey.bytes()
    );
    this._setQrCode(formattedArrayBufferToHexString(encrypt));
    this._nextRegistrationStep();
  };

  _saveAsPDF = async () => {
    const { qrCode, selectedWalletId } = this;
    if (!qrCode || !selectedWalletId) return;
    const selectedWallet = this.stores.wallets.getWalletById(selectedWalletId);
    if (!selectedWallet) return;
    const { name: walletName } = selectedWallet;
    const { desktopDirectoryPath } = this.stores.profile;
    const {
      currentLocale,
      currentDateFormat,
      currentTimeFormat,
    } = this.stores.profile;
    const nextVotingFundNumber = NEXT_VOTING_FUND_NUMBER;
    const { network, isMainnet } = this.environment;
    const intl = i18nContext(currentLocale);

    try {
      await votingPDFGenerator({
        nextVotingFundNumber,
        qrCode,
        walletName,
        currentLocale,
        currentDateFormat,
        currentTimeFormat,
        desktopDirectoryPath,
        network,
        isMainnet,
        intl,
      });
      this.actions.voting.saveAsPDFSuccess.trigger();
    } catch (error) {
      throw new Error(error);
    }
  };

  _checkVotingRegistrationTransaction = async () => {
    const {
      confirmations,
      state,
    }: WalletTransaction = await this.getTransactionRequest.execute({
      walletId: this.selectedWalletId,
      transactionId: this.transactionId,
    });

    // Update voting registration confirmations count
    if (this.transactionConfirmations !== confirmations) {
      this._setTransactionConfirmations(confirmations);
    }

    // Update voting registration pending state
    if (this.isTransactionPending && state === TransactionStates.OK) {
      this._setIsTransactionPending(false);
    }

    // Update voting registration confirmed state
    if (
      !this.isTransactionConfirmed &&
      confirmations >= VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS
    ) {
      this._setIsTransactionConfirmed(true);
      if (this.transactionPollingInterval)
        clearInterval(this.transactionPollingInterval);
    }
  };

  @action _checkFundPhase = (now: Date) => {
    const phaseValidation = {
      [FundPhases.SNAPSHOT]: (date: Date) => date < VOTING_CAST_START_DATE,
      [FundPhases.VOTING]: (date: Date) =>
        now >= VOTING_CAST_START_DATE && date < VOTING_CAST_END_DATE,
      [FundPhases.TALLYING]: (date: Date) =>
        now >= VOTING_CAST_END_DATE && date < VOTING_RESULTS_DATE,
      [FundPhases.RESULTS]: (date: Date) => date >= VOTING_RESULTS_DATE,
    };
    this.fundPhase =
      Object.keys(FundPhases)
        .map((key) => FundPhases[key])
        .find((phase) => phaseValidation[phase](now)) || FundPhases.SNAPSHOT;
  };

  _generateVotingRegistrationKey = async () => {
    const { Ed25519ExtendedPrivate: extendedPrivateKey } = await walletUtils;
    this._setVotingRegistrationKey(extendedPrivateKey.generate());
  };

  _getHexFromBech32 = async (key: string): Promise<string> => {
    const { bech32_decode_to_bytes: decodeBech32ToBytes } = await walletUtils;
    return formattedArrayBufferToHexString(decodeBech32ToBytes(key));
  };

  // GETTERS

  @computed get currentRoute(): string {
    return this.stores.router.location.pathname;
  }

  @computed get isVotingPage(): boolean {
    return this.currentRoute.indexOf(ROUTES.VOTING.REGISTRATION) > -1;
  }
}
<|endoftext|>


<commit_before>/**
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

/*global Session:false, chrome:true */
/* jshint evil:true */

(function() {
  'use strict';

  /**
   * Constants
   */

  var FLO_CONFIG_KEY = 'flo-config';

  /**
   * Flo client controller.
   *
   * @class FloClient
   * @private
   */

  function FloClient() {
    var self = this;
    loadConfig(function (config) {
      self.config = config;
      self.session = null;
      self.panelWindow = null;
      self.panelEventBuffer = [];
      self.status = self.status.bind(self);
      self.startNewSession = self.startNewSession.bind(self);
      self.createLogger = Logger(self.triggerEvent.bind(self, 'log'));
      self.loggger = self.createLogger('flo');
      self.createPanel();
      self.start();
    });
  }

  /**
   * Save current config to disk.
   *
   * @param {object} config
   * @private
   */

  FloClient.prototype.saveConfig = function() {
    saveConfig(this.config);
  };

  /**
   * Listen on the panel window for an event `type`, i.e. receive a message
   * from the panel.
   *
   * @param {string} type
   * @param {function} callback
   * @private
   */

  FloClient.prototype.listenToPanel = function(type, callback) {
    if (!this.panelWindow) {
      throw new Error('Panel not found');
    }
    this.panelWindow.addEventListener('flo_' + type, callback.bind(this));
  };

  /**
   * Trigger an event on the panel window, i.e. send a message to the panel.
   * If the panel wasn't instantiated yet, the event is buffered.
   *
   * @param {string} type
   * @param {object} data
   * @private
   */

  FloClient.prototype.triggerEvent = function(type, data) {
    var event = new Event('flo_' + type);
    event.data = data;
    // Save events for anytime we need to reinit the panel with prev state.
    this.panelEventBuffer.push(event);
    if (this.panelWindow) {
      this.panelWindow.dispatchEvent(event);
    }
    return event;
  };

  /**
   * Create a new panel.
   *
   * @param {function} callback
   * @private
   */

  FloClient.prototype.createPanel = function(callback) {
    var self = this;
    chrome.devtools.panels.create(
      'flo',
      '',
      'configure/configure.html',
      function (panel) {
        panel.onShown.addListener(function(panelWindow) {
          if (!panelWindow.wasShown) {
            self.panelWindow = panelWindow;
            self.initPanel();
            panelWindow.wasShown = true;
          }
        });
      }
    );
  };

  /**
   * Called after the panel is first created to listen on it's events.
   * Will also trigger all buffered events on the panel.
   *
   * @param {object} config
   * @private
   */

  FloClient.prototype.initPanel = function() {
    this.listenToPanel('config_changed', function(e) {
      this.config = e.data;
      this.saveConfig();
      this.startNewSession();
    });
    this.listenToPanel('retry', this.startNewSession);
    this.listenToPanel('enable_for_host', this.enableForHost);
    this.panelEventBuffer.forEach(function(event) {
      this.panelWindow.dispatchEvent(event);
    }, this);
    this.triggerEvent('load', this.config);
  };

  /**
   * Starts the flo client.
   *
   * @private
   */

  FloClient.prototype.start = function() {
    this.status('starting');
    this.startNewSession();
  };

  /**
   * Stops flo client.
   *
   * @private
   */

  FloClient.prototype.stop = function() {
    this.session.destroy();
    this.session = null;
  };

  /**
   * Get the url location of the inspected window.
   *
   * @param {function} callback
   * @private
   */

  FloClient.prototype.getLocation = function(callback) {
    chrome.devtools.inspectedWindow['eval'](
      'location.hostname || location.href',
      callback.bind(this)
    );
  };

  /**
   * Match config patterns against `host` and returns the matched site record.
   *
   * @param {string} host
   * @return {object|null}
   * @private
   */

  FloClient.prototype.getSite = function(host) {
    var config = this.config;
    for (var i = 0; i < config.sites.length; i++) {
      var site = config.sites[i];
      var pattern = parsePattern(site.pattern);
      var matched = false;
      if (pattern instanceof RegExp) {
        matched = pattern.exec(host);
      } else {
        matched = pattern === host;
      }
      if (matched) return site;
    }
    return null;
  };

  /**
   * Instantiates a new `session`.
   *
   * @private
   */

  FloClient.prototype.startNewSession = function() {
    if (this.session) {
      this.stop();
    }

    this.getLocation(
      function (host) {
        var site = this.getSite(host);
        if (site) {
          this.session = new Session(
            site.server || host,
            site.port || this.config.port,
            this.status,
            this.createLogger
          );
          this.session.start();
        } else {
          this.status('disabled');
        }
      }
    );
  };

  /**
   * Enables flo for the current inspected window host.
   *
   * @private
   */

  FloClient.prototype.enableForHost = function() {
    this.getLocation(function(host) {
      if (!this.getSite(host)) {
        this.config.sites.push({
          pattern: host,
          server: host
        });
        this.saveConfig();
        this.triggerEvent('load', this.config);
        this.startNewSession();
      }
    });
  };

  /**
   * Reports status changes to panel.
   *
   * @param {string} status
   * @param {object} aux
   * @private
   */

  FloClient.prototype.status = function(status, aux) {
    var text, action;
    switch (status) {
      case 'starting':
        text = 'Starting';
        break;
      case 'disabled':
        text = 'Disabled for this site';
        action = 'enable';
        break;
      case 'connecting':
        text = 'Connecting';
        break;
      case 'connected':
        text = 'Connected';
        break;
      case 'started':
        text = 'Started';
        break;
      case 'retry':
        text = 'Failed to connect, retrying in ' + (aux / 1000) + 's';
        break;
      case 'error':
        text = 'Error connecting';
        action = 'retry';
        break;
      default:
        throw new Error('Unknown session status.');
    }

    this.triggerEvent('status_change', {
      type: status,
      text: text,
      action: action
    });
  };

  /**
   * Save passed in config object to disk.
   *
   * @param {object} config
   * @private
   */

  function saveConfig(config) {
    chrome.runtime.sendMessage({
      name: 'localStorage:set',
      key: FLO_CONFIG_KEY,
      data: JSON.stringify(config)
    });
  }

  /**
   * Loads config from storage.
   *
   * @param {function} done
   * @private
   */

  function loadConfig(done) {
    var configJSON = tryToLoadLegacyConfig();

    if (configJSON) {
      var config = parseConfig(configJSON);
      // Persist new config to the new storage.
      saveConfig(config);
      setTimeout(done.bind(null, config), 0);
    }
    else {
      chrome.runtime.sendMessage(
        {
          name : 'localStorage:get',
          key : FLO_CONFIG_KEY
        },
        function (configJSON){
          var config = parseConfig(configJSON);
          done(config);
        }
      );
    }
  }

  /**
   * Parses config and sets sensible defaults.
   *
   * @param {string} config
   * @param {}
   * @private
   */

  function parseConfig(configJSON) {
    var config;

    try {
      config = JSON.parse(configJSON);
    }
    catch (ex) {
      config = {};
    }

    config.sites = config.sites || [];
    config.port = config.port || 8888;

    return config;
  }

  /**
   * Tries to load config from localstorage which was the old way of storing
   * config and returns false if it fails.
   *
   * @return {string} config
   * @private
   */

  function tryToLoadLegacyConfig() {;
    var config = null;

    try {
      var config = window.localStorage && localStorage.getItem(FLO_CONFIG_KEY);
      if (config) {
        localStorage.removeItem(FLO_CONFIG_KEY);
      }
    } catch (e) {
      return false;
    }

    return config;
  }

  /**
   * Optionally parses config from JSON to an object.
   * Also, parses patterns into regexp.
   *
   * @private
   * @return {object}
   */

  function parsePattern(pattern) {
    if (!pattern) return null;
    var m = pattern.match(/^\/(.+)\/([gim]{0,3})$/);
    if (m && m[1]) {
      return new RegExp(m[1], m[2]);
    }
    return pattern;
  }

  // Start the app.
  new FloClient();

})();
<commit_msg>Unnecessary semicolon.<commit_after>/**
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

/*global Session:false, chrome:true */
/* jshint evil:true */

(function() {
  'use strict';

  /**
   * Constants
   */

  var FLO_CONFIG_KEY = 'flo-config';

  /**
   * Flo client controller.
   *
   * @class FloClient
   * @private
   */

  function FloClient() {
    var self = this;
    loadConfig(function (config) {
      self.config = config;
      self.session = null;
      self.panelWindow = null;
      self.panelEventBuffer = [];
      self.status = self.status.bind(self);
      self.startNewSession = self.startNewSession.bind(self);
      self.createLogger = Logger(self.triggerEvent.bind(self, 'log'));
      self.loggger = self.createLogger('flo');
      self.createPanel();
      self.start();
    });
  }

  /**
   * Save current config to disk.
   *
   * @param {object} config
   * @private
   */

  FloClient.prototype.saveConfig = function() {
    saveConfig(this.config);
  };

  /**
   * Listen on the panel window for an event `type`, i.e. receive a message
   * from the panel.
   *
   * @param {string} type
   * @param {function} callback
   * @private
   */

  FloClient.prototype.listenToPanel = function(type, callback) {
    if (!this.panelWindow) {
      throw new Error('Panel not found');
    }
    this.panelWindow.addEventListener('flo_' + type, callback.bind(this));
  };

  /**
   * Trigger an event on the panel window, i.e. send a message to the panel.
   * If the panel wasn't instantiated yet, the event is buffered.
   *
   * @param {string} type
   * @param {object} data
   * @private
   */

  FloClient.prototype.triggerEvent = function(type, data) {
    var event = new Event('flo_' + type);
    event.data = data;
    // Save events for anytime we need to reinit the panel with prev state.
    this.panelEventBuffer.push(event);
    if (this.panelWindow) {
      this.panelWindow.dispatchEvent(event);
    }
    return event;
  };

  /**
   * Create a new panel.
   *
   * @param {function} callback
   * @private
   */

  FloClient.prototype.createPanel = function(callback) {
    var self = this;
    chrome.devtools.panels.create(
      'flo',
      '',
      'configure/configure.html',
      function (panel) {
        panel.onShown.addListener(function(panelWindow) {
          if (!panelWindow.wasShown) {
            self.panelWindow = panelWindow;
            self.initPanel();
            panelWindow.wasShown = true;
          }
        });
      }
    );
  };

  /**
   * Called after the panel is first created to listen on it's events.
   * Will also trigger all buffered events on the panel.
   *
   * @param {object} config
   * @private
   */

  FloClient.prototype.initPanel = function() {
    this.listenToPanel('config_changed', function(e) {
      this.config = e.data;
      this.saveConfig();
      this.startNewSession();
    });
    this.listenToPanel('retry', this.startNewSession);
    this.listenToPanel('enable_for_host', this.enableForHost);
    this.panelEventBuffer.forEach(function(event) {
      this.panelWindow.dispatchEvent(event);
    }, this);
    this.triggerEvent('load', this.config);
  };

  /**
   * Starts the flo client.
   *
   * @private
   */

  FloClient.prototype.start = function() {
    this.status('starting');
    this.startNewSession();
  };

  /**
   * Stops flo client.
   *
   * @private
   */

  FloClient.prototype.stop = function() {
    this.session.destroy();
    this.session = null;
  };

  /**
   * Get the url location of the inspected window.
   *
   * @param {function} callback
   * @private
   */

  FloClient.prototype.getLocation = function(callback) {
    chrome.devtools.inspectedWindow['eval'](
      'location.hostname || location.href',
      callback.bind(this)
    );
  };

  /**
   * Match config patterns against `host` and returns the matched site record.
   *
   * @param {string} host
   * @return {object|null}
   * @private
   */

  FloClient.prototype.getSite = function(host) {
    var config = this.config;
    for (var i = 0; i < config.sites.length; i++) {
      var site = config.sites[i];
      var pattern = parsePattern(site.pattern);
      var matched = false;
      if (pattern instanceof RegExp) {
        matched = pattern.exec(host);
      } else {
        matched = pattern === host;
      }
      if (matched) return site;
    }
    return null;
  };

  /**
   * Instantiates a new `session`.
   *
   * @private
   */

  FloClient.prototype.startNewSession = function() {
    if (this.session) {
      this.stop();
    }

    this.getLocation(
      function (host) {
        var site = this.getSite(host);
        if (site) {
          this.session = new Session(
            site.server || host,
            site.port || this.config.port,
            this.status,
            this.createLogger
          );
          this.session.start();
        } else {
          this.status('disabled');
        }
      }
    );
  };

  /**
   * Enables flo for the current inspected window host.
   *
   * @private
   */

  FloClient.prototype.enableForHost = function() {
    this.getLocation(function(host) {
      if (!this.getSite(host)) {
        this.config.sites.push({
          pattern: host,
          server: host
        });
        this.saveConfig();
        this.triggerEvent('load', this.config);
        this.startNewSession();
      }
    });
  };

  /**
   * Reports status changes to panel.
   *
   * @param {string} status
   * @param {object} aux
   * @private
   */

  FloClient.prototype.status = function(status, aux) {
    var text, action;
    switch (status) {
      case 'starting':
        text = 'Starting';
        break;
      case 'disabled':
        text = 'Disabled for this site';
        action = 'enable';
        break;
      case 'connecting':
        text = 'Connecting';
        break;
      case 'connected':
        text = 'Connected';
        break;
      case 'started':
        text = 'Started';
        break;
      case 'retry':
        text = 'Failed to connect, retrying in ' + (aux / 1000) + 's';
        break;
      case 'error':
        text = 'Error connecting';
        action = 'retry';
        break;
      default:
        throw new Error('Unknown session status.');
    }

    this.triggerEvent('status_change', {
      type: status,
      text: text,
      action: action
    });
  };

  /**
   * Save passed in config object to disk.
   *
   * @param {object} config
   * @private
   */

  function saveConfig(config) {
    chrome.runtime.sendMessage({
      name: 'localStorage:set',
      key: FLO_CONFIG_KEY,
      data: JSON.stringify(config)
    });
  }

  /**
   * Loads config from storage.
   *
   * @param {function} done
   * @private
   */

  function loadConfig(done) {
    var configJSON = tryToLoadLegacyConfig();

    if (configJSON) {
      var config = parseConfig(configJSON);
      // Persist new config to the new storage.
      saveConfig(config);
      setTimeout(done.bind(null, config), 0);
    }
    else {
      chrome.runtime.sendMessage(
        {
          name : 'localStorage:get',
          key : FLO_CONFIG_KEY
        },
        function (configJSON){
          var config = parseConfig(configJSON);
          done(config);
        }
      );
    }
  }

  /**
   * Parses config and sets sensible defaults.
   *
   * @param {string} config
   * @param {}
   * @private
   */

  function parseConfig(configJSON) {
    var config;

    try {
      config = JSON.parse(configJSON);
    }
    catch (ex) {
      config = {};
    }

    config.sites = config.sites || [];
    config.port = config.port || 8888;

    return config;
  }

  /**
   * Tries to load config from localstorage which was the old way of storing
   * config and returns false if it fails.
   *
   * @return {string} config
   * @private
   */

  function tryToLoadLegacyConfig() {
    var config = null;

    try {
      var config = window.localStorage && localStorage.getItem(FLO_CONFIG_KEY);
      if (config) {
        localStorage.removeItem(FLO_CONFIG_KEY);
      }
    } catch (e) {
      return false;
    }

    return config;
  }

  /**
   * Optionally parses config from JSON to an object.
   * Also, parses patterns into regexp.
   *
   * @private
   * @return {object}
   */

  function parsePattern(pattern) {
    if (!pattern) return null;
    var m = pattern.match(/^\/(.+)\/([gim]{0,3})$/);
    if (m && m[1]) {
      return new RegExp(m[1], m[2]);
    }
    return pattern;
  }

  // Start the app.
  new FloClient();

})();
<|endoftext|>


<commit_before>import {WaitingButtonController} from '../src/js/waiting-button.directive';
const angular = require('angular'),
      angularMock = require('angular-mocks');

describe('mbmWaitingButton', () => {
  beforeEach(() => {
    angular.mock.module('mbm.waitingButton');
  });

  it('should toggle waiting class based on operation promise', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });

  it('should support customizable waiting classes', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define the custom CSS class we'll be testing for
      const waitingClass = 'waiting';

      // Define simple template for the directive, and then compile it
      const template = `
        <button
          mbm-waiting-button="onClickReceiver()"
          waiting-button-waiting-class="${waitingClass}"
          >Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(waitingClass)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(waitingClass)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(waitingClass)).toEqual(false);
    });
  });

  it('should support prevent additional action triggers while in wait state', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      let receiverInvocationCounter = 0;
      $rootScope.onClickReceiver = function() {
        // TODO: This should updated to use a spy at some point.
        receiverInvocationCounter++;

        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Simulate another click
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the operation receiver has only been invoked once.
      expect(receiverInvocationCounter).toEqual(1);
    });
  });

  it('should support remove waiting presentation after error', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Reject (fail) the operation. Should move the waiting button to
      //  error state.
      operationDeferred.reject();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });
});
<commit_msg>Adds test for sequential success operations.<commit_after>import {WaitingButtonController} from '../src/js/waiting-button.directive';
const angular = require('angular'),
      angularMock = require('angular-mocks');

describe('mbmWaitingButton', () => {
  beforeEach(() => {
    angular.mock.module('mbm.waitingButton');
  });

  it('should toggle waiting class based on operation promise', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });

  it('should support customizable waiting classes', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define the custom CSS class we'll be testing for
      const waitingClass = 'waiting';

      // Define simple template for the directive, and then compile it
      const template = `
        <button
          mbm-waiting-button="onClickReceiver()"
          waiting-button-waiting-class="${waitingClass}"
          >Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(waitingClass)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(waitingClass)).toEqual(true);

      // Resolve the operation. Should return waiting button to normal state.
      operationDeferred.resolve();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(waitingClass)).toEqual(false);
    });
  });

  it('should support prevent additional action triggers while in wait state', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      let receiverInvocationCounter = 0;
      $rootScope.onClickReceiver = function() {
        // TODO: This should updated to use a spy at some point.
        receiverInvocationCounter++;

        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Simulate another click
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the operation receiver has only been invoked once.
      expect(receiverInvocationCounter).toEqual(1);
    });
  });

  it('should support remove waiting presentation after error', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      const operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Verify that the element doesn't have the waiting-button loading class
      //  by default.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

      // Simulate click to enter "waiting" state
      element[0].click();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element now has the waiting-button loading class.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

      // Reject (fail) the operation. Should move the waiting button to
      //  error state.
      operationDeferred.reject();

      // Run the Angular digest loop once to pick up the changes.
      $rootScope.$digest();

      // Verify that the element doesn't have the waiting-button loading class
      //  after the operation promise has resolved.
      expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
    });
  });

  it('should support sequential success operations', () => {
    angular.mock.inject(($rootScope, $compile, $timeout, $q) => {
      // Build a Promise to represent an async operation
      let operationDeferred = $q.defer();

      // Fake receiver for the click action
      $rootScope.onClickReceiver = function() {
        return operationDeferred.promise;
      }

      // Define simple template for the directive, and then compile it
      const template = `
        <button mbm-waiting-button="onClickReceiver()">Waiting Button</button>
      `;
      const element = $compile(template)($rootScope);

      // Run the click test once
      successClickTest();

      // Reset the operation promise
      operationDeferred = $q.defer();

      // Run the click test again
      successClickTest();

      function successClickTest() {
        // Verify that the element doesn't have the waiting-button loading class
        //  by default.
        expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);

        // Simulate click to enter "waiting" state
        element[0].click();

        // Run the Angular digest loop once to pick up the changes.
        $rootScope.$digest();

        // Verify that the element now has the waiting-button loading class.
        expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(true);

        // Resolve the operation. Should return waiting button to normal state.
        operationDeferred.resolve();

        // Run the Angular digest loop once to pick up the changes.
        $rootScope.$digest();

        // Verify that the element doesn't have the waiting-button loading class
        //  after the operation promise has resolved.
        expect(element.hasClass(WaitingButtonController.WAITING_CLASS)).toEqual(false);
      }
    });
  });
});
<|endoftext|>


<commit_before>import { Template } from 'meteor/templating'
import { UserData, ActivityIPs } from '/imports/api/indexDB.js'
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

import './flaggedUsers.html'

Template.flaggedUsers.onCreated(function() {
	this.autorun(() => {
		SubsCache.subscribe('userData')
		SubsCache.subscribe('users')
		SubsCache.subscribe('activityIPs')
	})

	this.sort = new ReactiveVar('user-desc')
})

// to prevent code redundancy, we create a method to determine active flags
const activeFlags = function() {
	let flags = []

	// we can only nest two levels
	Object.keys(this.info.flags || {}).forEach(i => {
		if (typeof this.info.flags[i] === 'object') {
			Object.keys(this.info.flags[i]).forEach(j => {
				if (this.info.flags[i][j]) {
					flags.push(`${i}.${j}`)
				}
			})
		} else {
			if (this.info.flags[i]) {
				flags.push(i)
			}
		}
	})

	return flags
}

Template.flaggedUsers.events({
	'change #js-sort': (event, templateInstance) => {
        event.preventDefault()

        templateInstance.sort.set($(event.currentTarget).val())
    }
})

Template.flaggedUsers.helpers({
	ipAddresses: function() {
		let users = Meteor.users.find({
			$or: [{
				suspended: false
			}, {
				suspended: {
					$exists: false
				}
			}] // don't include banned users
		}).fetch().map(i => {
			return {
				user: i,
				info: (UserData.findOne({
					_id: i._id
				}) || {})
			}
		}).filter(i => {
			return _.intersection(activeFlags.call(i), ['duplicate.createdIP', 'duplicate.accessIP']).length > 0 // find all users with flags
		})

		let ips = _.uniq(_.flatten(users.map(i => ((i.info.sessionData || []).map(j => j.loggedIP))))) // return all flagged ip addresses
		let ignored = ActivityIPs.find({
			ignored: true,
			$or: [{
				time: {
					$gt: new Date() - 1000*60*60*24*30
				}
			}, {
				whitelist: true
			}]
		}).fetch().map(i => i.ip)

		ips = ips.filter(i => !~ignored.indexOf(i)) // filter out ignored ips

		return ips.map(i => {
			let us = users.filter(j => j.info.sessionData && j.info.sessionData.some(k => k.loggedIP === i))

			if (us.length > 1) {
				const lastAccess = us.map(i => i.info.sessionData[i.info.sessionData.length - 1].time).sort((i1, i2) => i2 - i1)[0]
				const ip = ActivityIPs.findOne({
					ip: i
				}) || {}

				return {
					ip: i,
					lastAccess: moment(lastAccess).fromNow(),
					lastAccessIP: moment(ip.lastAccess).fromNow(),
					users: us.length,
					lastAccessSort: lastAccess,
					lastAccessIPSort: ip.lastAccess
				}
			} else {
				return false
			}
		}).filter(i => !!i).sort((i1, i2) => {
			let sort = Template.instance().sort.get()

			if (sort === 'user-desc') return i2.lastAccessSort - i1.lastAccessSort
			if (sort === 'user-asc') return i1.lastAccessSort - i2.lastAccessSort
			if (sort === 'ip-desc') return i2.lastAccessIPSort - i1.lastAccessIPSort
			if (sort === 'ip-asc') return i1.lastAccessIPSort - i2.lastAccessIPSort
		})
	}
})
<commit_msg>Problem: Can't sort suspicious IP's by IP login time (#1983)<commit_after>import { Template } from 'meteor/templating'
import { UserData, ActivityIPs } from '/imports/api/indexDB.js'
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

import './flaggedUsers.html'

Template.flaggedUsers.onCreated(function() {
	this.autorun(() => {
		SubsCache.subscribe('userData')
		SubsCache.subscribe('users')
		SubsCache.subscribe('activityIPs')
	})

	this.sort = new ReactiveVar('user-desc')
})

// to prevent code redundancy, we create a method to determine active flags
const activeFlags = function() {
	let flags = []

	// we can only nest two levels
	Object.keys(this.info.flags || {}).forEach(i => {
		if (typeof this.info.flags[i] === 'object') {
			Object.keys(this.info.flags[i]).forEach(j => {
				if (this.info.flags[i][j]) {
					flags.push(`${i}.${j}`)
				}
			})
		} else {
			if (this.info.flags[i]) {
				flags.push(i)
			}
		}
	})

	return flags
}

Template.flaggedUsers.events({
	'change #js-sort': (event, templateInstance) => {
        event.preventDefault()

        templateInstance.sort.set($(event.currentTarget).val())
    }
})

Template.flaggedUsers.helpers({
	ipAddresses: function() {
		let users = Meteor.users.find({
			$or: [{
				suspended: false
			}, {
				suspended: {
					$exists: false
				}
			}] // don't include banned users
		}).fetch().map(i => {
			return {
				user: i,
				info: (UserData.findOne({
					_id: i._id
				}) || {})
			}
		}).filter(i => {
			return _.intersection(activeFlags.call(i), ['duplicate.createdIP', 'duplicate.accessIP']).length > 0 // find all users with flags
		})

		let ips = _.uniq(_.flatten(users.map(i => ((i.info.sessionData || []).map(j => j.loggedIP))))) // return all flagged ip addresses
		let ignored = ActivityIPs.find({
			ignored: true,
			$or: [{
				time: {
					$gt: new Date() - 1000*60*60*24*30
				}
			}, {
				whitelist: true
			}]
		}).fetch().map(i => i.ip)

		ips = ips.filter(i => !~ignored.indexOf(i)) // filter out ignored ips

		return ips.map(i => {
			let us = users.filter(j => j.info.sessionData && j.info.sessionData.some(k => k.loggedIP === i))

			if (us.length > 1) {
				const lastAccess = us.map(i => i.info.sessionData[i.info.sessionData.length - 1].time).sort((i1, i2) => i2 - i1)[0]
				const ip = ActivityIPs.findOne({
					ip: i
				}) || {}

				return {
					ip: i,
					lastAccess: moment(lastAccess).fromNow(),
					lastAccessIP: ip.lastAccess ? moment(ip.lastAccess).fromNow() : '-',
					users: us.length,
					lastAccessSort: lastAccess,
					lastAccessIPSort: ip.lastAccess || 0
				}
			} else {
				return false
			}
		}).filter(i => !!i).sort((i1, i2) => {
			let sort = Template.instance().sort.get()

			if (sort === 'user-desc') return i2.lastAccessSort - i1.lastAccessSort
			if (sort === 'user-asc') return i1.lastAccessSort - i2.lastAccessSort
			if (sort === 'ip-desc') return i2.lastAccessIPSort - i1.lastAccessIPSort
			if (sort === 'ip-asc') return i1.lastAccessIPSort - i2.lastAccessIPSort
		})
	}
})
<|endoftext|>


<commit_before>'use strict';

var feedbackUrl = process.env.FEEDBACK_URL || 'https://hootch.test.netflix.net/submit';
var gateHost = process.env.API_HOST || 'https://spinnaker-api-prestaging.mgmttest.netflix.net';
var bakeryDetailUrl = process.env.BAKERY_DETAIL_URL || 'http://bakery.test.netflix.net/#/?region={{context.region}}&package={{context.package}}&detail=bake:{{context.status.resourceId}}';
var authEndpoint = process.env.AUTH_ENDPOINT || 'https://spinnaker-api-prestaging.mgmttest.netflix.net/auth/user';

window.spinnakerSettings = {
  checkForUpdates: true,
  defaultProviders: ['aws', 'gce', 'azure', 'cf', 'kubernetes', 'titus', 'openstack'],
  feedbackUrl: feedbackUrl,
  gateUrl: gateHost,
  bakeryDetailUrl: bakeryDetailUrl,
  authEndpoint: authEndpoint,
  pollSchedule: 30000,
  defaultTimeZone: process.env.TIMEZONE || 'America/Los_Angeles', // see http://momentjs.com/timezone/docs/#/data-utilities/
  defaultCategory: 'serverGroup',
  defaultInstancePort: 80,
  providers: {
    azure: {
      defaults: {
        account: 'azure-test',
        region: 'westus'
      },
    },
    aws: {
      defaults: {
        account: 'test',
        region: 'us-east-1'
      },
      defaultSecurityGroups: ['nf-datacenter-vpc', 'nf-infrastructure-vpc', 'nf-datacenter', 'nf-infrastructure'],
      loadBalancers: {
        // if true, VPC load balancers will be created as internal load balancers if the selected subnet has a purpose
        // tag that starts with "internal"
        inferInternalFlagFromSubnet: false,
      },
      useAmiBlockDeviceMappings: false,
    },
    gce: {
      defaults: {
        account: 'my-google-account',
        region: 'us-central1',
        zone: 'us-central1-f',
      },
    },
    titus: {
      defaults: {
        account: 'titustestvpc',
        region: 'us-east-1'
      },
    },
    openstack: {
      defaults: {
        account: 'test',
        region: 'us-west-1'
      },
    },
    kubernetes: {
      defaults: {
        account: 'my-kubernetes-account',
        namespace: 'default'
      },
    }
  },
  whatsNew: {
    gistId: '32526cd608db3d811b38',
    fileName: 'news.md',
  },
  notifications: {
    email: {
      enabled: true,
    },
    hipchat: {
      enabled: true,
      botName: 'Skynet T-800'
    },
    sms: {
      enabled: true,
    },
    slack: {
      enabled: true,
      botName: 'spinnakerbot'
    }
  },
  authEnabled: true,
  authTtl: 600000,
  gitSources: ['stash', 'github'],
  triggerTypes: ['git', 'pipeline', 'docker', 'cron', 'jenkins'],
  feature: {
    pipelines: true,
    notifications: false,
    fastProperty: true,
    vpcMigrator: true,
    clusterDiff: true,
    roscoMode: false,
    netflixMode: false,
    // whether stages affecting infrastructure (like "Create Load Balancer") should be enabled or not
    infrastructureStages: process.env.INFRA_STAGES === 'enabled',
    jobs: false,
    serialization: false,
  },
};
<commit_msg>(core) allow auth to be disabled via environment variable (#2492)<commit_after>'use strict';

var feedbackUrl = process.env.FEEDBACK_URL || 'https://hootch.test.netflix.net/submit';
var gateHost = process.env.API_HOST || 'https://spinnaker-api-prestaging.mgmttest.netflix.net';
var bakeryDetailUrl = process.env.BAKERY_DETAIL_URL || 'http://bakery.test.netflix.net/#/?region={{context.region}}&package={{context.package}}&detail=bake:{{context.status.resourceId}}';
var authEndpoint = process.env.AUTH_ENDPOINT || 'https://spinnaker-api-prestaging.mgmttest.netflix.net/auth/user';
var authEnabled = process.env.AUTH_ENABLED === 'false' ? false : true;

window.spinnakerSettings = {
  checkForUpdates: true,
  defaultProviders: ['aws', 'gce', 'azure', 'cf', 'kubernetes', 'titus', 'openstack'],
  feedbackUrl: feedbackUrl,
  gateUrl: gateHost,
  bakeryDetailUrl: bakeryDetailUrl,
  authEndpoint: authEndpoint,
  pollSchedule: 30000,
  defaultTimeZone: process.env.TIMEZONE || 'America/Los_Angeles', // see http://momentjs.com/timezone/docs/#/data-utilities/
  defaultCategory: 'serverGroup',
  defaultInstancePort: 80,
  providers: {
    azure: {
      defaults: {
        account: 'azure-test',
        region: 'westus'
      },
    },
    aws: {
      defaults: {
        account: 'test',
        region: 'us-east-1'
      },
      defaultSecurityGroups: ['nf-datacenter-vpc', 'nf-infrastructure-vpc', 'nf-datacenter', 'nf-infrastructure'],
      loadBalancers: {
        // if true, VPC load balancers will be created as internal load balancers if the selected subnet has a purpose
        // tag that starts with "internal"
        inferInternalFlagFromSubnet: false,
      },
      useAmiBlockDeviceMappings: false,
    },
    gce: {
      defaults: {
        account: 'my-google-account',
        region: 'us-central1',
        zone: 'us-central1-f',
      },
    },
    titus: {
      defaults: {
        account: 'titustestvpc',
        region: 'us-east-1'
      },
    },
    openstack: {
      defaults: {
        account: 'test',
        region: 'us-west-1'
      },
    },
    kubernetes: {
      defaults: {
        account: 'my-kubernetes-account',
        namespace: 'default'
      },
    }
  },
  whatsNew: {
    gistId: '32526cd608db3d811b38',
    fileName: 'news.md',
  },
  notifications: {
    email: {
      enabled: true,
    },
    hipchat: {
      enabled: true,
      botName: 'Skynet T-800'
    },
    sms: {
      enabled: true,
    },
    slack: {
      enabled: true,
      botName: 'spinnakerbot'
    }
  },
  authEnabled: authEnabled,
  authTtl: 600000,
  gitSources: ['stash', 'github'],
  triggerTypes: ['git', 'pipeline', 'docker', 'cron', 'jenkins'],
  feature: {
    pipelines: true,
    notifications: false,
    fastProperty: true,
    vpcMigrator: true,
    clusterDiff: true,
    roscoMode: false,
    netflixMode: false,
    // whether stages affecting infrastructure (like "Create Load Balancer") should be enabled or not
    infrastructureStages: process.env.INFRA_STAGES === 'enabled',
    jobs: false,
    serialization: false,
  },
};
<|endoftext|>


<commit_before>import React from 'react';
import { Map, ScaleControl } from 'react-leaflet';
import { connect } from 'react-redux';
import { ToastContainer, ToastMessage } from 'react-toastr';
import queryString from 'query-string';

import Navbar from 'react-bootstrap/lib/Navbar';
import Row from 'react-bootstrap/lib/Row';
import Nav from 'react-bootstrap/lib/Nav';
import NavItem from 'react-bootstrap/lib/NavItem';
import NavDropdown from 'react-bootstrap/lib/NavDropdown';
import MenuItem from 'react-bootstrap/lib/MenuItem';

import NavbarHeader from 'fm3/components/NavbarHeader';
import Layers from 'fm3/components/Layers';
import FontAwesomeIcon from 'fm3/components/FontAwesomeIcon';
import ProgressIndicator from 'fm3/components/ProgressIndicator';

import SearchMenu from 'fm3/components/SearchMenu';
import SearchResults from 'fm3/components/SearchResults';

import ObjectsMenu from 'fm3/components/ObjectsMenu';
import ObjectsResult from 'fm3/components/ObjectsResult';

import MeasurementMenu from 'fm3/components/MeasurementMenu';
import DistanceMeasurementResult from 'fm3/components/DistanceMeasurementResult';
import AreaMeasurementResult from 'fm3/components/AreaMeasurementResult';
import ElevationMeasurementResult from 'fm3/components/ElevationMeasurementResult';

import RoutePlannerMenu from 'fm3/components/RoutePlannerMenu';
import RoutePlannerResult from 'fm3/components/RoutePlannerResult';
import Settings from 'fm3/components/Settings';

import * as FmPropTypes from 'fm3/propTypes';
import mapEventEmitter from 'fm3/emitters/mapEventEmitter';

import { mapRefocus } from 'fm3/actions/mapActions';
import { setTool } from 'fm3/actions/mainActions';
import { setActivePopup } from 'fm3/actions/mainActions';

import { baseLayers, overlayLayers } from 'fm3/mapDefinitions';
import { setLeafletElement } from 'fm3/leafletElementHolder';

import 'fm3/styles/main.scss';

const ToastMessageFactory = React.createFactory(ToastMessage.animation);

class Main extends React.Component {

  static propTypes = {
    lat: React.PropTypes.number,
    lon: React.PropTypes.number,
    zoom: React.PropTypes.number,
    match: React.PropTypes.object,
    history: React.PropTypes.object,
    tool: React.PropTypes.string,
    tileFormat: FmPropTypes.tileFormat.isRequired,
    overlays: FmPropTypes.overlays,
    overlayOpacity: React.PropTypes.any,
    mapType: FmPropTypes.mapType.isRequired,
    onSetTool: React.PropTypes.func.isRequired,
    onMapRefocus: React.PropTypes.func.isRequired,
    activePopup: React.PropTypes.string,
    onLaunchPopup: React.PropTypes.func.isRequired,
    progress: React.PropTypes.bool,
  };

  componentWillMount() {
    // set redux according to URL
    this.props.onMapRefocus(getMapDiff(this.props));
  }

  componentWillUnmount() {
    setLeafletElement(null);
  }

  componentDidMount() {
    setLeafletElement(this.refs.map.leafletElement);
  }

  componentWillReceiveProps(newProps) {
    this.ignoreMapMoveEndEvent = true;

    const stateChanged = [ 'mapType', 'overlays', 'zoom', 'lat', 'lon' ].some(prop => newProps[prop] !== this.props[prop]);
    if (stateChanged) {
      // update URL
      updateUrl(newProps);
    } else {
      // set redux according to URL
      const changes = getMapDiff(newProps);
      if (Object.keys(changes).length) {
        newProps.onMapRefocus(changes);
      }
    }
  }

  componentDidUpdate() {
    this.ignoreMapMoveEndEvent = false;
  }

  handleMapMoveEnd() {
    if (this.ignoreMapMoveEndEvent) {
      return;
    }

    const map = this.refs.map.leafletElement;
    const { lat, lng: lon } = map.getCenter();
    const zoom = map.getZoom();

    if (this.props.lat !== lat || this.props.lon !== lon || this.props.zoom !== zoom) {
      this.props.onMapRefocus({ lat, lon, zoom });
    }
  }

  handleMapTypeChange(mapType) {
    if (this.props.mapType !== mapType) {
      this.props.onMapRefocus({ mapType });
    }
  }

  handleOverlayChange(overlays) {
    this.props.onMapRefocus({ overlays });
  }

  handleMapClick({ latlng: { lat, lng: lon } }) {
    mapEventEmitter.emit('mapClick', lat, lon);
  }

  // handlePoiSearch() {
  //   if (this.props.zoom < 12) {
  //     this.showToast('info', null, 'Vyhľadávanie miest funguje až od zoom úrovne 12');
  //   } else {
  //     this.props.onSetTool('objects');
  //   }
  // }

  showToast(toastType, line1, line2) {
    this.refs.toastContainer[toastType](
      line2,
      line1, // sic!
      { timeOut: 3000, showAnimation: 'animated fadeIn', hideAnimation: 'animated fadeOut' }
    );
  }

  handleToolSet(tool) {
    this.props.onSetTool(this.props.tool === tool ? null : tool); // toggle tool
  }

  render() {
    const { tool, tileFormat, activePopup, onLaunchPopup, progress } = this.props;
    const b = (fn, ...args) => fn.bind(this, ...args);
    const showDefaultMenu = [ null, 'select-home-location' ].indexOf(tool) !== -1;

    return (

        <Row>
          <Navbar fluid style={{ marginBottom: 0 }}>
            <NavbarHeader/>
            <Navbar.Collapse>
              {tool === 'objects' && <ObjectsMenu onShowToast={b(this.showToast)}/>}
              {(showDefaultMenu || tool === 'search') && <SearchMenu/>}
              {tool === 'route-planner' && <RoutePlannerMenu onShowToast={b(this.showToast)}/>}
              {(tool === 'measure' || tool === 'measure-ele' || tool === 'measure-area') && <MeasurementMenu/>}
              {activePopup === 'settings' && <Settings onShowToast={b(this.showToast)}/>}
              {showDefaultMenu &&

                  <NavItem onClick={b(this.handleToolSet, 'objects')}>
                  <FontAwesomeIcon icon="map-marker"/> Miesta
                  </NavItem>
                  <NavItem onClick={b(this.handleToolSet, 'route-planner')}>
                    <FontAwesomeIcon icon="map-signs"/> Plánovač
                  </NavItem>
                  <NavItem onClick={b(this.handleToolSet, 'measure')}>
                    <FontAwesomeIcon icon="arrows-h"/> Meranie
                  </NavItem>

              }
              {showDefaultMenu &&

                  <NavDropdown title="Viac" id="additional-menu-items">
                    <MenuItem onClick={() => onLaunchPopup('settings')}><FontAwesomeIcon icon="cog"/> Nastavenia</MenuItem>
                    <MenuItem divider></MenuItem>
                    <MenuItem onClick={() => window.open('http://wiki.freemap.sk/NahlasenieChyby')}><FontAwesomeIcon icon="exclamation-triangle"/> Nahlás chybu</MenuItem>
                  </NavDropdown>

              }
            </Navbar.Collapse>
          </Navbar>
        </Row>
        <Row>
          <ProgressIndicator active={progress}/>
        </Row>
        <Row className={`map-holder tool-${tool || 'none'} active-map-type-${this.props.mapType}`}>
          <Map
            ref="map"
            center={L.latLng(this.props.lat, this.props.lon)}
            zoom={this.props.zoom}
            onMoveend={b(this.handleMapMoveEnd)}
            onClick={b(this.handleMapClick)}
          >
            <Layers
              mapType={this.props.mapType} onMapChange={b(this.handleMapTypeChange)}
              overlays={this.props.overlays} onOverlaysChange={b(this.handleOverlayChange)}
              tileFormat={tileFormat} overlayOpacity={this.props.overlayOpacity}
            />

            {(tool === 'measure' || tool === 'measure-ele' || tool === 'measure-area') && <ScaleControl imperial={false} position="bottomright"/> }

            {(showDefaultMenu || tool === 'search') && <SearchResults/>}

            {tool === 'objects' && <ObjectsResult/>}

            {tool === 'route-planner' && <RoutePlannerResult onShowToast={b(this.showToast)}/>}

            {tool === 'measure' && <DistanceMeasurementResult/>}

            {tool === 'measure-ele' && <ElevationMeasurementResult/>}

            {tool === 'measure-area' && <AreaMeasurementResult onShowToast={b(this.showToast)}/>}
          </Map>
        </Row>

        <ToastContainer
          ref="toastContainer"
          toastMessageFactory={ToastMessageFactory}
          className="toast-top-right"/>

    );
  }
}

export default connect(
  function (state) {
    return {
      lat: state.map.lat,
      lon: state.map.lon,
      zoom: state.map.zoom,
      tool: state.main.tool,
      mapType: state.map.mapType,
      overlays: state.map.overlays,
      overlayOpacity: state.map.overlayOpacity,
      tileFormat: state.map.tileFormat,
      activePopup: state.main.activePopup,
      progress: state.main.progress,
    };
  },
  function (dispatch) {
    return {
      onSetTool(tool) {
        dispatch(setTool(tool));
      },
      onMapRefocus(changes) {
        dispatch(mapRefocus(changes));
      },
      onLaunchPopup(popupName) {
        dispatch(setActivePopup(popupName));
      }
    };
  }
)(Main);

const baseLetters = baseLayers.map(({ type }) => type).join('');
const overlayLetters = overlayLayers.map(({ type }) => type).join('');
const layersRegExp = new RegExp(`^[${baseLetters}][${overlayLetters}]*$`);

function getMapDiff(props) {
  const { location: { search } } = props;

  const query = queryString.parse(search);

  const [ zoomFrag, latFrag, lonFrag ] = (query.map || '').split('/');

  const lat = parseFloat(latFrag);
  const lon = parseFloat(lonFrag);
  const zoom = parseInt(zoomFrag);

  const layers = query.layers || '';

  const layersOK = layersRegExp.test(layers);

  if (!layersOK || isNaN(lat) || isNaN(lon) || isNaN(zoom)) {
    updateUrl(props);
    return {};
  }

  const mapType = layers.charAt(0);
  const overlays = layers.length > 1 ? layers.substring(1).split('') : [];

  const changes = {};

  if (mapType !== props.mapType) {
    changes.mapType = mapType;
  }

  if (overlays.join('') !== props.overlays.join('')) {
    changes.overlays = overlays;
  }

  if (Math.abs(lat - props.lat) > 0.00001) {
    changes.lat = lat;
  }

  if (Math.abs(lon - props.lon) > 0.00001) {
    changes.lon = lon;
  }

  if (zoom !== props.zoom) {
    changes.zoom = zoom;
  }

  return changes;
}

function updateUrl(props) {
  const { mapType, overlays, zoom, lat, lon } = props;
  props.history.replace({
    search: `?map=${zoom}/${lat.toFixed(5)}/${lon.toFixed(5)}&layers=${mapType}${overlays.join('')}`
  });
}
<commit_msg>85: show scale all the time<commit_after>import React from 'react';
import { Map, ScaleControl } from 'react-leaflet';
import { connect } from 'react-redux';
import { ToastContainer, ToastMessage } from 'react-toastr';
import queryString from 'query-string';

import Navbar from 'react-bootstrap/lib/Navbar';
import Row from 'react-bootstrap/lib/Row';
import Nav from 'react-bootstrap/lib/Nav';
import NavItem from 'react-bootstrap/lib/NavItem';
import NavDropdown from 'react-bootstrap/lib/NavDropdown';
import MenuItem from 'react-bootstrap/lib/MenuItem';

import NavbarHeader from 'fm3/components/NavbarHeader';
import Layers from 'fm3/components/Layers';
import FontAwesomeIcon from 'fm3/components/FontAwesomeIcon';
import ProgressIndicator from 'fm3/components/ProgressIndicator';

import SearchMenu from 'fm3/components/SearchMenu';
import SearchResults from 'fm3/components/SearchResults';

import ObjectsMenu from 'fm3/components/ObjectsMenu';
import ObjectsResult from 'fm3/components/ObjectsResult';

import MeasurementMenu from 'fm3/components/MeasurementMenu';
import DistanceMeasurementResult from 'fm3/components/DistanceMeasurementResult';
import AreaMeasurementResult from 'fm3/components/AreaMeasurementResult';
import ElevationMeasurementResult from 'fm3/components/ElevationMeasurementResult';

import RoutePlannerMenu from 'fm3/components/RoutePlannerMenu';
import RoutePlannerResult from 'fm3/components/RoutePlannerResult';
import Settings from 'fm3/components/Settings';

import * as FmPropTypes from 'fm3/propTypes';
import mapEventEmitter from 'fm3/emitters/mapEventEmitter';

import { mapRefocus } from 'fm3/actions/mapActions';
import { setTool } from 'fm3/actions/mainActions';
import { setActivePopup } from 'fm3/actions/mainActions';

import { baseLayers, overlayLayers } from 'fm3/mapDefinitions';
import { setLeafletElement } from 'fm3/leafletElementHolder';

import 'fm3/styles/main.scss';

const ToastMessageFactory = React.createFactory(ToastMessage.animation);

class Main extends React.Component {

  static propTypes = {
    lat: React.PropTypes.number,
    lon: React.PropTypes.number,
    zoom: React.PropTypes.number,
    match: React.PropTypes.object,
    history: React.PropTypes.object,
    tool: React.PropTypes.string,
    tileFormat: FmPropTypes.tileFormat.isRequired,
    overlays: FmPropTypes.overlays,
    overlayOpacity: React.PropTypes.any,
    mapType: FmPropTypes.mapType.isRequired,
    onSetTool: React.PropTypes.func.isRequired,
    onMapRefocus: React.PropTypes.func.isRequired,
    activePopup: React.PropTypes.string,
    onLaunchPopup: React.PropTypes.func.isRequired,
    progress: React.PropTypes.bool,
  };

  componentWillMount() {
    // set redux according to URL
    this.props.onMapRefocus(getMapDiff(this.props));
  }

  componentWillUnmount() {
    setLeafletElement(null);
  }

  componentDidMount() {
    setLeafletElement(this.refs.map.leafletElement);
  }

  componentWillReceiveProps(newProps) {
    this.ignoreMapMoveEndEvent = true;

    const stateChanged = [ 'mapType', 'overlays', 'zoom', 'lat', 'lon' ].some(prop => newProps[prop] !== this.props[prop]);
    if (stateChanged) {
      // update URL
      updateUrl(newProps);
    } else {
      // set redux according to URL
      const changes = getMapDiff(newProps);
      if (Object.keys(changes).length) {
        newProps.onMapRefocus(changes);
      }
    }
  }

  componentDidUpdate() {
    this.ignoreMapMoveEndEvent = false;
  }

  handleMapMoveEnd() {
    if (this.ignoreMapMoveEndEvent) {
      return;
    }

    const map = this.refs.map.leafletElement;
    const { lat, lng: lon } = map.getCenter();
    const zoom = map.getZoom();

    if (this.props.lat !== lat || this.props.lon !== lon || this.props.zoom !== zoom) {
      this.props.onMapRefocus({ lat, lon, zoom });
    }
  }

  handleMapTypeChange(mapType) {
    if (this.props.mapType !== mapType) {
      this.props.onMapRefocus({ mapType });
    }
  }

  handleOverlayChange(overlays) {
    this.props.onMapRefocus({ overlays });
  }

  handleMapClick({ latlng: { lat, lng: lon } }) {
    mapEventEmitter.emit('mapClick', lat, lon);
  }

  // handlePoiSearch() {
  //   if (this.props.zoom < 12) {
  //     this.showToast('info', null, 'Vyhľadávanie miest funguje až od zoom úrovne 12');
  //   } else {
  //     this.props.onSetTool('objects');
  //   }
  // }

  showToast(toastType, line1, line2) {
    this.refs.toastContainer[toastType](
      line2,
      line1, // sic!
      { timeOut: 3000, showAnimation: 'animated fadeIn', hideAnimation: 'animated fadeOut' }
    );
  }

  handleToolSet(tool) {
    this.props.onSetTool(this.props.tool === tool ? null : tool); // toggle tool
  }

  render() {
    const { tool, tileFormat, activePopup, onLaunchPopup, progress } = this.props;
    const b = (fn, ...args) => fn.bind(this, ...args);
    const showDefaultMenu = [ null, 'select-home-location' ].indexOf(tool) !== -1;

    return (

        <Row>
          <Navbar fluid style={{ marginBottom: 0 }}>
            <NavbarHeader/>
            <Navbar.Collapse>
              {tool === 'objects' && <ObjectsMenu onShowToast={b(this.showToast)}/>}
              {(showDefaultMenu || tool === 'search') && <SearchMenu/>}
              {tool === 'route-planner' && <RoutePlannerMenu onShowToast={b(this.showToast)}/>}
              {(tool === 'measure' || tool === 'measure-ele' || tool === 'measure-area') && <MeasurementMenu/>}
              {activePopup === 'settings' && <Settings onShowToast={b(this.showToast)}/>}
              {showDefaultMenu &&

                  <NavItem onClick={b(this.handleToolSet, 'objects')}>
                  <FontAwesomeIcon icon="map-marker"/> Miesta
                  </NavItem>
                  <NavItem onClick={b(this.handleToolSet, 'route-planner')}>
                    <FontAwesomeIcon icon="map-signs"/> Plánovač
                  </NavItem>
                  <NavItem onClick={b(this.handleToolSet, 'measure')}>
                    <FontAwesomeIcon icon="arrows-h"/> Meranie
                  </NavItem>

              }
              {showDefaultMenu &&

                  <NavDropdown title="Viac" id="additional-menu-items">
                    <MenuItem onClick={() => onLaunchPopup('settings')}><FontAwesomeIcon icon="cog"/> Nastavenia</MenuItem>
                    <MenuItem divider></MenuItem>
                    <MenuItem onClick={() => window.open('http://wiki.freemap.sk/NahlasenieChyby')}><FontAwesomeIcon icon="exclamation-triangle"/> Nahlás chybu</MenuItem>
                  </NavDropdown>

              }
            </Navbar.Collapse>
          </Navbar>
        </Row>
        <Row>
          <ProgressIndicator active={progress}/>
        </Row>
        <Row className={`map-holder tool-${tool || 'none'} active-map-type-${this.props.mapType}`}>
          <Map
            ref="map"
            center={L.latLng(this.props.lat, this.props.lon)}
            zoom={this.props.zoom}
            onMoveend={b(this.handleMapMoveEnd)}
            onClick={b(this.handleMapClick)}
          >
            <Layers
              mapType={this.props.mapType} onMapChange={b(this.handleMapTypeChange)}
              overlays={this.props.overlays} onOverlaysChange={b(this.handleOverlayChange)}
              tileFormat={tileFormat} overlayOpacity={this.props.overlayOpacity}
            />

            <ScaleControl imperial={false} position="bottomright" />

            {(showDefaultMenu || tool === 'search') && <SearchResults/>}

            {tool === 'objects' && <ObjectsResult/>}

            {tool === 'route-planner' && <RoutePlannerResult onShowToast={b(this.showToast)}/>}

            {tool === 'measure' && <DistanceMeasurementResult/>}

            {tool === 'measure-ele' && <ElevationMeasurementResult/>}

            {tool === 'measure-area' && <AreaMeasurementResult onShowToast={b(this.showToast)}/>}
          </Map>
        </Row>

        <ToastContainer
          ref="toastContainer"
          toastMessageFactory={ToastMessageFactory}
          className="toast-top-right"/>

    );
  }
}

export default connect(
  function (state) {
    return {
      lat: state.map.lat,
      lon: state.map.lon,
      zoom: state.map.zoom,
      tool: state.main.tool,
      mapType: state.map.mapType,
      overlays: state.map.overlays,
      overlayOpacity: state.map.overlayOpacity,
      tileFormat: state.map.tileFormat,
      activePopup: state.main.activePopup,
      progress: state.main.progress,
    };
  },
  function (dispatch) {
    return {
      onSetTool(tool) {
        dispatch(setTool(tool));
      },
      onMapRefocus(changes) {
        dispatch(mapRefocus(changes));
      },
      onLaunchPopup(popupName) {
        dispatch(setActivePopup(popupName));
      }
    };
  }
)(Main);

const baseLetters = baseLayers.map(({ type }) => type).join('');
const overlayLetters = overlayLayers.map(({ type }) => type).join('');
const layersRegExp = new RegExp(`^[${baseLetters}][${overlayLetters}]*$`);

function getMapDiff(props) {
  const { location: { search } } = props;

  const query = queryString.parse(search);

  const [ zoomFrag, latFrag, lonFrag ] = (query.map || '').split('/');

  const lat = parseFloat(latFrag);
  const lon = parseFloat(lonFrag);
  const zoom = parseInt(zoomFrag);

  const layers = query.layers || '';

  const layersOK = layersRegExp.test(layers);

  if (!layersOK || isNaN(lat) || isNaN(lon) || isNaN(zoom)) {
    updateUrl(props);
    return {};
  }

  const mapType = layers.charAt(0);
  const overlays = layers.length > 1 ? layers.substring(1).split('') : [];

  const changes = {};

  if (mapType !== props.mapType) {
    changes.mapType = mapType;
  }

  if (overlays.join('') !== props.overlays.join('')) {
    changes.overlays = overlays;
  }

  if (Math.abs(lat - props.lat) > 0.00001) {
    changes.lat = lat;
  }

  if (Math.abs(lon - props.lon) > 0.00001) {
    changes.lon = lon;
  }

  if (zoom !== props.zoom) {
    changes.zoom = zoom;
  }

  return changes;
}

function updateUrl(props) {
  const { mapType, overlays, zoom, lat, lon } = props;
  props.history.replace({
    search: `?map=${zoom}/${lat.toFixed(5)}/${lon.toFixed(5)}&layers=${mapType}${overlays.join('')}`
  });
}
<|endoftext|>


<commit_before>import $ from 'dom7';
import ConstructorMethods from '../../utils/constructor-methods';
import Calendar from './calendar-class';

export default {
  name: 'calendar',
  static: {
    Calendar,
  },
  create() {
    const app = this;
    app.calendar = ConstructorMethods({
      defaultSelector: '.calendar',
      constructor: Calendar,
      app,
      domProp: 'f7Calendar',
    });
    app.calendar.close = function close(el = '.calendar') {
      const $el = $(el);
      if ($el.length === 0) return;
      const calendar = $el[0].f7Calendar;
      if (!calendar || (calendar && !calendar.opened)) return;
      calendar.close();
    };
  },
  params: {
    calendar: {
      // Calendar settings
      monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
      monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
      dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
      dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
      firstDay: 1, // First day of the week, Monday
      weekendDays: [0, 6], // Sunday and Saturday
      multiple: false,
      rangePicker: false,
      dateFormat: 'yyyy-mm-dd',
      direction: 'horizontal', // or 'vertical'
      minDate: null,
      maxDate: null,
      disabled: null, // dates range of disabled days
      events: null, // dates range of days with events
      rangesClasses: null, // array with custom classes date ranges
      touchMove: true,
      animate: true,
      closeOnSelect: false,
      monthSelector: true,
      yearSelector: true,
      weekHeader: true,
      value: null,
      // Common opener settings
      containerEl: null,
      openIn: 'auto', // or 'popover' or 'sheet' or 'customModal'
      formatValue: null,
      inputEl: null,
      inputReadOnly: true,
      closeByOutsideClick: true,
      scrollToInput: true,
      header: false,
      headerPlaceholder: 'Select date',
      footer: false,
      toolbar: true,
      toolbarCloseText: 'Done',
      cssClass: null,
      routableModals: true,
      view: null,
      url: 'date/',
      // Render functions
      renderWeekHeader: null,
      renderMonths: null,
      renderMonth: null,
      renderMonthSelector: null,
      renderYearSelector: null,
      renderHeader: null,
      renderFooter: null,
      renderToolbar: null,
      renderInline: null,
      renderPopover: null,
      renderSheet: null,
      render: null,
    },
  },
};
<commit_msg>add new parameters for calendar<commit_after>import $ from 'dom7';
import ConstructorMethods from '../../utils/constructor-methods';
import Calendar from './calendar-class';

export default {
  name: 'calendar',
  static: {
    Calendar,
  },
  create() {
    const app = this;
    app.calendar = ConstructorMethods({
      defaultSelector: '.calendar',
      constructor: Calendar,
      app,
      domProp: 'f7Calendar',
    });
    app.calendar.close = function close(el = '.calendar') {
      const $el = $(el);
      if ($el.length === 0) return;
      const calendar = $el[0].f7Calendar;
      if (!calendar || (calendar && !calendar.opened)) return;
      calendar.close();
    };
  },
  params: {
    calendar: {
      // Calendar settings
      calendarType: 'georgian', // or 'jalali'
      monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
      monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
      dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
      dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
      firstDay: 1, // First day of the week, Monday
      weekendDays: [0, 6], // Sunday and Saturday
      multiple: false,
      rangePicker: false,
      minimumDaysInRange: 1, // when calendar is used as rangePicker
      maximumDaysInRange: 0, // when calendar is used as rangePicker, 0 means unlimited
      dateFormat: 'yyyy-mm-dd',
      direction: 'horizontal', // or 'vertical'
      minDate: null,
      maxDate: null,
      disabled: null, // dates range of disabled days
      events: null, // dates range of days with events
      rangesClasses: null, // array with custom classes date ranges
      touchMove: true,
      animate: true,
      closeOnSelect: false,
      monthSelector: true,
      yearSelector: true,
      weekHeader: true,
      value: null,
      // Common opener settings
      containerEl: null,
      openIn: 'auto', // or 'popover' or 'sheet' or 'customModal'
      formatValue: null,
      inputEl: null,
      inputReadOnly: true,
      closeByOutsideClick: true,
      scrollToInput: true,
      header: false,
      headerPlaceholder: 'Select date',
      footer: false,
      toolbar: true,
      toolbarCloseText: 'Done',
      cssClass: null,
      routableModals: true,
      view: null,
      url: 'date/',
      // Render functions
      renderWeekHeader: null,
      renderMonths: null,
      renderMonth: null,
      renderMonthSelector: null,
      renderYearSelector: null,
      renderHeader: null,
      renderFooter: null,
      renderToolbar: null,
      renderInline: null,
      renderPopover: null,
      renderSheet: null,
      render: null,
    },
  },
};
<|endoftext|>