<commit_before>import config from 'config';
import { get } from 'lodash';

import * as constants from '../../constants/transactions';
import logger from '../../lib/logger';
import { getApplicationFee, getHostFee, getHostFeeSharePercent, getPlatformTip } from '../../lib/payments';
import stripe, { convertFromStripeAmount, convertToStripeAmount, extractFees } from '../../lib/stripe';
import models from '../../models';

import { refundTransaction, refundTransactionOnlyInDatabase } from './common';

const UNKNOWN_ERROR_MSG = 'Something went wrong with the payment, please contact support@opencollective.com.';

/**
 * Get or create a customer under the platform stripe account
 */
const getOrCreateCustomerOnPlatformAccount = async ({ paymentMethod, user, collective }) => {
  if (paymentMethod.customerId) {
    return stripe.customers.retrieve(paymentMethod.customerId);
  }

  const payload = { source: paymentMethod.token };
  if (user) {
    payload.email = user.email;
  }
  if (collective) {
    payload.description = `https://opencollective.com/${collective.slug}`;
  }

  const customer = await stripe.customers.create(payload);

  paymentMethod.customerId = customer.id;
  await paymentMethod.update({ customerId: customer.id });

  return customer;
};

/**
 * Get the customerId for the Stripe Account of the Host
 * Or create one using the Stripe token associated with the platform (paymentMethod.token)
 * and saves it under PaymentMethod.data[hostStripeAccount.username]
 * @param {*} hostStripeAccount
 */
const getOrCreateCustomerOnHostAccount = async (hostStripeAccount, { paymentMethod, user }) => {
  // Customers pre-migration will have their stripe user connected
  // to the platform stripe account, not to the host's stripe
  // account. Since payment methods had no name before that
  // migration, we're using it to test for pre-migration users;

  // Well, DISCARD what is written above, these customers are coming from the Host
  if (!paymentMethod.name) {
    const customer = await stripe.customers.retrieve(paymentMethod.customerId, {
      stripeAccount: hostStripeAccount.username,
    });

    if (customer) {
      logger.info(`Pre-migration customer found: ${paymentMethod.customerId}`);
      logger.info(JSON.stringify(customer));
      return customer;
    }

    logger.info(`Pre-migration customer not found: ${paymentMethod.customerId}`);
    return { id: paymentMethod.customerId };
  }

  const data = paymentMethod.data || {};
  data.customerIdForHost = data.customerIdForHost || {};
  if (data.customerIdForHost[hostStripeAccount.username]) {
    return stripe.customers.retrieve(data.customerIdForHost[hostStripeAccount.username], {
      stripeAccount: hostStripeAccount.username,
    });
  } else {
    const platformStripeCustomer = await getOrCreateCustomerOnPlatformAccount({
      paymentMethod,
      user,
    });

    let customer;

    // This is a special case where the account is the root account
    if (hostStripeAccount.username === config.stripe.accountId) {
      customer = platformStripeCustomer;
    }

    // This is the normal case where we create a customer on the host connected account
    if (!customer) {
      // More info about that
      // - Documentation: https://stripe.com/docs/connect/shared-customers
      // - API: https://stripe.com/docs/api/tokens/create_card
      const token = await stripe.tokens.create(
        { customer: platformStripeCustomer.id },
        { stripeAccount: hostStripeAccount.username },
      );

      customer = await stripe.customers.create(
        { source: token.id, email: user.email },
        { stripeAccount: hostStripeAccount.username },
      );
    }

    data.customerIdForHost[hostStripeAccount.username] = customer.id;
    paymentMethod.data = data;
    await paymentMethod.update({ data });

    return customer;
  }
};

/**
 * Returns a Promise with the transaction created
 * Note: we need to create a token for hostStripeAccount because paymentMethod.customerId is a customer of the platform
 * See: Shared Customers: https://stripe.com/docs/connect/shared-customers
 */
const createChargeAndTransactions = async (hostStripeAccount, { order, hostStripeCustomer }) => {
  const host = await order.collective.getHostCollective();
  const hostFeeSharePercent = await getHostFeeSharePercent(order, host);
  const isSharedRevenue = !!hostFeeSharePercent;

  // Read or compute Platform Fee
  const applicationFee = await getApplicationFee(order, host);
  const platformTip = getPlatformTip(order);

  // Make sure data is available (breaking in some old tests)
  order.data = order.data || {};

  /* eslint-disable camelcase */

  let paymentIntent = order.data.paymentIntent;
  if (!paymentIntent) {
    const createPayload = {
      amount: convertToStripeAmount(order.currency, order.totalAmount),
      currency: order.currency,
      customer: hostStripeCustomer.id,
      description: order.description,
      confirm: false,
      confirmation_method: 'manual',
      metadata: {
        from: `${config.host.website}/${order.fromCollective.slug}`,
        to: `${config.host.website}/${order.collective.slug}`,
      },
    };
    // We don't add a platform fee if the host is the root account
    if (applicationFee && hostStripeAccount.username !== config.stripe.accountId) {
      createPayload.application_fee_amount = convertToStripeAmount(order.currency, applicationFee);
    }
    if (order.interval) {
      createPayload.setup_future_usage = 'off_session';
    } else if (!order.processedAt && order.data.savePaymentMethod) {
      createPayload.setup_future_usage = 'on_session';
    }
    // Add Payment Method ID if it's available
    const paymentMethodId = get(hostStripeCustomer, 'default_source', get(hostStripeCustomer, 'sources.data[0].id'));
    if (paymentMethodId) {
      createPayload.payment_method = paymentMethodId;
    } else {
      logger.info('paymentMethod is missing in hostStripeCustomer to pass to Payment Intent.');
      logger.info(JSON.stringify(hostStripeCustomer));
    }
    paymentIntent = await stripe.paymentIntents.create(createPayload, {
      stripeAccount: hostStripeAccount.username,
    });
  }
  paymentIntent = await stripe.paymentIntents.confirm(paymentIntent.id, {
    stripeAccount: hostStripeAccount.username,
  });

  /* eslint-enable camelcase */

  if (paymentIntent.next_action) {
    order.data.paymentIntent = { id: paymentIntent.id, status: paymentIntent.status };
    await order.update({ data: order.data });
    const paymentIntentError = new Error('Payment Intent require action');
    paymentIntentError.stripeAccount = hostStripeAccount.username;
    paymentIntentError.stripeResponse = { paymentIntent };
    throw paymentIntentError;
  }

  if (paymentIntent.status !== 'succeeded') {
    logger.error('Unknown error with Stripe Payment Intent.');
    logger.error(paymentIntent);
    throw new Error(UNKNOWN_ERROR_MSG);
  }

  const charge = paymentIntent.charges.data[0];

  const balanceTransaction = await stripe.balanceTransactions.retrieve(charge.balance_transaction, {
    stripeAccount: hostStripeAccount.username,
  });

  // Create a Transaction
  const amount = order.totalAmount;
  const currency = order.currency;
  const hostCurrency = balanceTransaction.currency.toUpperCase();
  const amountInHostCurrency = convertFromStripeAmount(balanceTransaction.currency, balanceTransaction.amount);
  const hostCurrencyFxRate = amountInHostCurrency / order.totalAmount;

  const hostFee = await getHostFee(order, host);
  const hostFeeInHostCurrency = Math.round(hostFee * hostCurrencyFxRate);

  const data = {
    charge,
    balanceTransaction,
    isFeesOnTop: order.data?.isFeesOnTop,
    isSharedRevenue,
    settled: true,
    platformFee: applicationFee, // TODO: to be removed
    applicationFee,
    platformTip,
    hostFeeSharePercent,
  };

  const fees = extractFees(balanceTransaction, balanceTransaction.currency);
  const platformFeeInHostCurrency = isSharedRevenue ? platformTip * hostCurrencyFxRate || 0 : fees.applicationFee;
  const paymentProcessorFeeInHostCurrency = fees.stripeFee;

  const transactionPayload = {
    CreatedByUserId: order.CreatedByUserId,
    FromCollectiveId: order.FromCollectiveId,
    CollectiveId: order.CollectiveId,
    PaymentMethodId: order.PaymentMethodId,
    type: constants.TransactionTypes.CREDIT,
    OrderId: order.id,
    amount,
    currency,
    hostCurrency,
    amountInHostCurrency,
    hostCurrencyFxRate,
    paymentProcessorFeeInHostCurrency,
    taxAmount: order.taxAmount,
    description: order.description,
    hostFeeInHostCurrency,
    platformFeeInHostCurrency,
    data,
  };

  return models.Transaction.createFromContributionPayload(transactionPayload, {
    isPlatformRevenueDirectlyCollected: true,
  });
};

export const setupCreditCard = async (paymentMethod, { user, collective } = {}) => {
  const platformStripeCustomer = await getOrCreateCustomerOnPlatformAccount({
    paymentMethod,
    user,
    collective,
  });

  const paymentMethodId = platformStripeCustomer.sources.data[0].id;

  let setupIntent;
  if (paymentMethod.data.setupIntent) {
    setupIntent = await stripe.setupIntents.retrieve(paymentMethod.data.setupIntent.id);
    // TO CHECK: what happens if the setupIntent is not found
  }
  if (!setupIntent) {
    setupIntent = await stripe.setupIntents.create({
      customer: platformStripeCustomer.id,
      payment_method: paymentMethodId, // eslint-disable-line camelcase
      confirm: true,
    });
  }

  if (
    !paymentMethod.data.setupIntent ||
    paymentMethod.data.setupIntent.id !== setupIntent.id ||
    paymentMethod.data.setupIntent.status !== setupIntent.status
  ) {
    paymentMethod.data.setupIntent = { id: setupIntent.id, status: setupIntent.status };
    await paymentMethod.update({ data: paymentMethod.data });
  }

  if (setupIntent.next_action) {
    const setupIntentError = new Error('Setup Intent require action');
    setupIntentError.stripeResponse = { setupIntent };
    throw setupIntentError;
  }

  return paymentMethod;
};

export default {
  features: {
    recurring: true,
    waitToCharge: false,
  },

  processOrder: async order => {
    const hostStripeAccount = await order.collective.getHostStripeAccount();

    const hostStripeCustomer = await getOrCreateCustomerOnHostAccount(hostStripeAccount, {
      paymentMethod: order.paymentMethod,
      user: order.createdByUser,
    });

    let transactions;
    try {
      transactions = await createChargeAndTransactions(hostStripeAccount, {
        order,
        hostStripeCustomer,
      });
    } catch (error) {
      // Here, we check strictly the error message
      const knownErrors = [
        'Your card has insufficient funds.',
        'Your card was declined.',
        'Your card does not support this type of purchase.',
        'Your card has expired.',
        "Your card's security code is incorrect.",
        'Your card number is incorrect.',
        'The zip code you supplied failed validation.',
        'Invalid amount.',
        'Payment Intent require action',
      ];

      if (knownErrors.includes(error.message)) {
        throw error;
      }

      // Here, we do a partial check and rewrite the error.
      const identifiedErrors = {
        // This object cannot be accessed right now because another API request or Stripe process is currently accessing it.
        // If you see this error intermittently, retry the request.
        // If you see this error frequently and are making multiple concurrent requests to a single object, make your requests serially or at a lower rate.
        'This object cannot be accessed right now because another API request or Stripe process is currently accessing it.':
          'Payment Processing error (API request).',
        // You cannot confirm this PaymentIntent because it's missing a payment method.
        // To confirm the PaymentIntent with cus_9cNHqpdWYOV4aH, specify a payment method attached to this customer along with the customer ID.
        "You cannot confirm this PaymentIntent because it's missing a payment method.":
          'Internal Payment error (invalid PaymentIntent)',
        // You have exceeded the maximum number of declines on this card in the last 24 hour period.
        // Please contact us via https://support.stripe.com/contact if you need further assistance.
        'You have exceeded the maximum number of declines on this card': 'Your card was declined.',
        // An error occurred while processing your card. Try again in a little bit.
        'An error occurred while processing your card.': 'Payment Processing error (API error).',
        // This account cannot currently make live charges.
        // If you are a customer trying to make a purchase, please contact the owner of this site.
        // Your transaction has not been processed.
        'This account cannot currently make live charges.': 'Payment Processing error (Host error).',
      };
      const errorKey = Object.keys(identifiedErrors).find(errorMessage => error.message.includes(errorMessage));
      if (errorKey) {
        throw new Error(identifiedErrors[errorKey]);
      }

      logger.error(`Unknown Stripe Payment Error: ${error.message}`);
      logger.error(error);
      logger.error(error.stack);

      throw new Error(UNKNOWN_ERROR_MSG);
    }

    await order.paymentMethod.update({
      confirmedAt: new Date(),
      saved: order.paymentMethod.saved || Boolean(order.data?.savePaymentMethod),
    });

    return transactions;
  },

  refundTransaction,
  refundTransactionOnlyInDatabase,
  webhook: (/* requestBody, event */) => {
    // We don't do anything at the moment
    return Promise.resolve();
  },
};
<commit_msg>Add `decline_code` and Error Details for Stripe Errors (#6183)<commit_after>import config from 'config';
import { get } from 'lodash';

import * as constants from '../../constants/transactions';
import logger from '../../lib/logger';
import { getApplicationFee, getHostFee, getHostFeeSharePercent, getPlatformTip } from '../../lib/payments';
import stripe, { convertFromStripeAmount, convertToStripeAmount, extractFees } from '../../lib/stripe';
import models from '../../models';

import { refundTransaction, refundTransactionOnlyInDatabase } from './common';

const UNKNOWN_ERROR_MSG = 'Something went wrong with the payment, please contact support@opencollective.com.';

/**
 * Get or create a customer under the platform stripe account
 */
const getOrCreateCustomerOnPlatformAccount = async ({ paymentMethod, user, collective }) => {
  if (paymentMethod.customerId) {
    return stripe.customers.retrieve(paymentMethod.customerId);
  }

  const payload = { source: paymentMethod.token };
  if (user) {
    payload.email = user.email;
  }
  if (collective) {
    payload.description = `https://opencollective.com/${collective.slug}`;
  }

  const customer = await stripe.customers.create(payload);

  paymentMethod.customerId = customer.id;
  await paymentMethod.update({ customerId: customer.id });

  return customer;
};

/**
 * Get the customerId for the Stripe Account of the Host
 * Or create one using the Stripe token associated with the platform (paymentMethod.token)
 * and saves it under PaymentMethod.data[hostStripeAccount.username]
 * @param {*} hostStripeAccount
 */
const getOrCreateCustomerOnHostAccount = async (hostStripeAccount, { paymentMethod, user }) => {
  // Customers pre-migration will have their stripe user connected
  // to the platform stripe account, not to the host's stripe
  // account. Since payment methods had no name before that
  // migration, we're using it to test for pre-migration users;

  // Well, DISCARD what is written above, these customers are coming from the Host
  if (!paymentMethod.name) {
    const customer = await stripe.customers.retrieve(paymentMethod.customerId, {
      stripeAccount: hostStripeAccount.username,
    });

    if (customer) {
      logger.info(`Pre-migration customer found: ${paymentMethod.customerId}`);
      logger.info(JSON.stringify(customer));
      return customer;
    }

    logger.info(`Pre-migration customer not found: ${paymentMethod.customerId}`);
    return { id: paymentMethod.customerId };
  }

  const data = paymentMethod.data || {};
  data.customerIdForHost = data.customerIdForHost || {};
  if (data.customerIdForHost[hostStripeAccount.username]) {
    return stripe.customers.retrieve(data.customerIdForHost[hostStripeAccount.username], {
      stripeAccount: hostStripeAccount.username,
    });
  } else {
    const platformStripeCustomer = await getOrCreateCustomerOnPlatformAccount({
      paymentMethod,
      user,
    });

    let customer;

    // This is a special case where the account is the root account
    if (hostStripeAccount.username === config.stripe.accountId) {
      customer = platformStripeCustomer;
    }

    // This is the normal case where we create a customer on the host connected account
    if (!customer) {
      // More info about that
      // - Documentation: https://stripe.com/docs/connect/shared-customers
      // - API: https://stripe.com/docs/api/tokens/create_card
      const token = await stripe.tokens.create(
        { customer: platformStripeCustomer.id },
        { stripeAccount: hostStripeAccount.username },
      );

      customer = await stripe.customers.create(
        { source: token.id, email: user.email },
        { stripeAccount: hostStripeAccount.username },
      );
    }

    data.customerIdForHost[hostStripeAccount.username] = customer.id;
    paymentMethod.data = data;
    await paymentMethod.update({ data });

    return customer;
  }
};

/**
 * Returns a Promise with the transaction created
 * Note: we need to create a token for hostStripeAccount because paymentMethod.customerId is a customer of the platform
 * See: Shared Customers: https://stripe.com/docs/connect/shared-customers
 */
const createChargeAndTransactions = async (hostStripeAccount, { order, hostStripeCustomer }) => {
  const host = await order.collective.getHostCollective();
  const hostFeeSharePercent = await getHostFeeSharePercent(order, host);
  const isSharedRevenue = !!hostFeeSharePercent;

  // Read or compute Platform Fee
  const applicationFee = await getApplicationFee(order, host);
  const platformTip = getPlatformTip(order);

  // Make sure data is available (breaking in some old tests)
  order.data = order.data || {};

  /* eslint-disable camelcase */

  let paymentIntent = order.data.paymentIntent;
  if (!paymentIntent) {
    const createPayload = {
      amount: convertToStripeAmount(order.currency, order.totalAmount),
      currency: order.currency,
      customer: hostStripeCustomer.id,
      description: order.description,
      confirm: false,
      confirmation_method: 'manual',
      metadata: {
        from: `${config.host.website}/${order.fromCollective.slug}`,
        to: `${config.host.website}/${order.collective.slug}`,
      },
    };
    // We don't add a platform fee if the host is the root account
    if (applicationFee && hostStripeAccount.username !== config.stripe.accountId) {
      createPayload.application_fee_amount = convertToStripeAmount(order.currency, applicationFee);
    }
    if (order.interval) {
      createPayload.setup_future_usage = 'off_session';
    } else if (!order.processedAt && order.data.savePaymentMethod) {
      createPayload.setup_future_usage = 'on_session';
    }
    // Add Payment Method ID if it's available
    const paymentMethodId = get(hostStripeCustomer, 'default_source', get(hostStripeCustomer, 'sources.data[0].id'));
    if (paymentMethodId) {
      createPayload.payment_method = paymentMethodId;
    } else {
      logger.info('paymentMethod is missing in hostStripeCustomer to pass to Payment Intent.');
      logger.info(JSON.stringify(hostStripeCustomer));
    }
    paymentIntent = await stripe.paymentIntents.create(createPayload, {
      stripeAccount: hostStripeAccount.username,
    });
  }
  paymentIntent = await stripe.paymentIntents.confirm(paymentIntent.id, {
    stripeAccount: hostStripeAccount.username,
  });

  /* eslint-enable camelcase */

  if (paymentIntent.next_action) {
    order.data.paymentIntent = { id: paymentIntent.id, status: paymentIntent.status };
    await order.update({ data: order.data });
    const paymentIntentError = new Error('Payment Intent require action');
    paymentIntentError.stripeAccount = hostStripeAccount.username;
    paymentIntentError.stripeResponse = { paymentIntent };
    throw paymentIntentError;
  }

  if (paymentIntent.status !== 'succeeded') {
    logger.error('Unknown error with Stripe Payment Intent.');
    logger.error(paymentIntent);
    throw new Error(UNKNOWN_ERROR_MSG);
  }

  const charge = paymentIntent.charges.data[0];

  const balanceTransaction = await stripe.balanceTransactions.retrieve(charge.balance_transaction, {
    stripeAccount: hostStripeAccount.username,
  });

  // Create a Transaction
  const amount = order.totalAmount;
  const currency = order.currency;
  const hostCurrency = balanceTransaction.currency.toUpperCase();
  const amountInHostCurrency = convertFromStripeAmount(balanceTransaction.currency, balanceTransaction.amount);
  const hostCurrencyFxRate = amountInHostCurrency / order.totalAmount;

  const hostFee = await getHostFee(order, host);
  const hostFeeInHostCurrency = Math.round(hostFee * hostCurrencyFxRate);

  const data = {
    charge,
    balanceTransaction,
    isFeesOnTop: order.data?.isFeesOnTop,
    isSharedRevenue,
    settled: true,
    platformFee: applicationFee, // TODO: to be removed
    applicationFee,
    platformTip,
    hostFeeSharePercent,
  };

  const fees = extractFees(balanceTransaction, balanceTransaction.currency);
  const platformFeeInHostCurrency = isSharedRevenue ? platformTip * hostCurrencyFxRate || 0 : fees.applicationFee;
  const paymentProcessorFeeInHostCurrency = fees.stripeFee;

  const transactionPayload = {
    CreatedByUserId: order.CreatedByUserId,
    FromCollectiveId: order.FromCollectiveId,
    CollectiveId: order.CollectiveId,
    PaymentMethodId: order.PaymentMethodId,
    type: constants.TransactionTypes.CREDIT,
    OrderId: order.id,
    amount,
    currency,
    hostCurrency,
    amountInHostCurrency,
    hostCurrencyFxRate,
    paymentProcessorFeeInHostCurrency,
    taxAmount: order.taxAmount,
    description: order.description,
    hostFeeInHostCurrency,
    platformFeeInHostCurrency,
    data,
  };

  return models.Transaction.createFromContributionPayload(transactionPayload, {
    isPlatformRevenueDirectlyCollected: true,
  });
};

export const setupCreditCard = async (paymentMethod, { user, collective } = {}) => {
  const platformStripeCustomer = await getOrCreateCustomerOnPlatformAccount({
    paymentMethod,
    user,
    collective,
  });

  const paymentMethodId = platformStripeCustomer.sources.data[0].id;

  let setupIntent;
  if (paymentMethod.data.setupIntent) {
    setupIntent = await stripe.setupIntents.retrieve(paymentMethod.data.setupIntent.id);
    // TO CHECK: what happens if the setupIntent is not found
  }
  if (!setupIntent) {
    setupIntent = await stripe.setupIntents.create({
      customer: platformStripeCustomer.id,
      payment_method: paymentMethodId, // eslint-disable-line camelcase
      confirm: true,
    });
  }

  if (
    !paymentMethod.data.setupIntent ||
    paymentMethod.data.setupIntent.id !== setupIntent.id ||
    paymentMethod.data.setupIntent.status !== setupIntent.status
  ) {
    paymentMethod.data.setupIntent = { id: setupIntent.id, status: setupIntent.status };
    await paymentMethod.update({ data: paymentMethod.data });
  }

  if (setupIntent.next_action) {
    const setupIntentError = new Error('Setup Intent require action');
    setupIntentError.stripeResponse = { setupIntent };
    throw setupIntentError;
  }

  return paymentMethod;
};

export default {
  features: {
    recurring: true,
    waitToCharge: false,
  },

  processOrder: async order => {
    const hostStripeAccount = await order.collective.getHostStripeAccount();

    let transactions;
    try {
      const hostStripeCustomer = await getOrCreateCustomerOnHostAccount(hostStripeAccount, {
        paymentMethod: order.paymentMethod,
        user: order.createdByUser,
      });

      transactions = await createChargeAndTransactions(hostStripeAccount, {
        order,
        hostStripeCustomer,
      });
    } catch (error) {
      // Here, we check strictly the error message
      const knownErrors = [
        'Your card has insufficient funds.',
        'Your card was declined.',
        'Your card does not support this type of purchase.',
        'Your card has expired.',
        "Your card's security code is incorrect.",
        'Your card number is incorrect.',
        'The zip code you supplied failed validation.',
        'Invalid amount.',
        'Payment Intent require action',
      ];

      if (knownErrors.includes(error.message)) {
        logger.error(
          `Stripe Error (handled): ${error.type}, Message: ${error.message}, Decline Code: ${error.decline_code}, Code: ${error.code}`,
        );
        throw error;
      }

      // Here, we do a partial check and rewrite the error.
      const identifiedErrors = {
        // This object cannot be accessed right now because another API request or Stripe process is currently accessing it.
        // If you see this error intermittently, retry the request.
        // If you see this error frequently and are making multiple concurrent requests to a single object, make your requests serially or at a lower rate.
        'This object cannot be accessed right now because another API request or Stripe process is currently accessing it.':
          'Payment Processing error (API request).',
        // You cannot confirm this PaymentIntent because it's missing a payment method.
        // To confirm the PaymentIntent with cus_9cNHqpdWYOV4aH, specify a payment method attached to this customer along with the customer ID.
        "You cannot confirm this PaymentIntent because it's missing a payment method.":
          'Internal Payment error (invalid PaymentIntent)',
        // You have exceeded the maximum number of declines on this card in the last 24 hour period.
        // Please contact us via https://support.stripe.com/contact if you need further assistance.
        'You have exceeded the maximum number of declines on this card': 'Your card was declined.',
        // An error occurred while processing your card. Try again in a little bit.
        'An error occurred while processing your card.': 'Payment Processing error (API error).',
        // This account cannot currently make live charges.
        // If you are a customer trying to make a purchase, please contact the owner of this site.
        // Your transaction has not been processed.
        'This account cannot currently make live charges.': 'Payment Processing error (Host error).',
      };
      const errorKey = Object.keys(identifiedErrors).find(errorMessage => error.message.includes(errorMessage));
      if (errorKey) {
        throw new Error(identifiedErrors[errorKey]);
      }

      logger.error(`Unknown Stripe Payment Error: ${error.message}`);
      logger.error(error);
      logger.error(error.stack);

      throw new Error(UNKNOWN_ERROR_MSG);
    }

    await order.paymentMethod.update({
      confirmedAt: new Date(),
      saved: order.paymentMethod.saved || Boolean(order.data?.savePaymentMethod),
    });

    return transactions;
  },

  refundTransaction,
  refundTransactionOnlyInDatabase,
  webhook: (/* requestBody, event */) => {
    // We don't do anything at the moment
    return Promise.resolve();
  },
};
<|endoftext|>


<commit_before>const execSync = require('child_process').execSync
const chalk = require('chalk')
const ansiEscapes = require('ansi-escapes')
const debounce = require('lodash.debounce')
const gitFiles = require('git-files')
const gitDiffGlob = require('git-diff-glob')
const keypress = require('terminal-keypress')
const jumper = require('terminal-jumper')
const pager = require('node-pager')
const utils = require('./utils')

class Interactive {
	constructor(options = {}) {
		this.options = options
		this.fileIndex = 0
		this.showingIndicator = false
		this.showingDiff = false

		this.instructions = `${chalk.gray('Arrow keys to navigate')}\n${chalk.gray('Tab to show diff of selected file(s)')}`
		this.gitAddPrompt = `${chalk.green('Enter a file glob: ')}`

		this.onType = debounce(this.onType.bind(this), 200)
		this.onTab = this.onTab.bind(this)
		this.onArrow = this.onArrow.bind(this)
		this.exitBeforeAdd = this.exitBeforeAdd.bind(this)
		this.exitAfterAdd = this.exitAfterAdd.bind(this)

		keypress.init()
		keypress.on('exit', () => this.showCursor())
		keypress.on('exit', this.exitBeforeAdd)
	}

	run() {
		let stagedFiles = gitFiles.staged()

		if (stagedFiles.length > 0) {
			this.showAlreadyAddedFilesWarning(stagedFiles)
		} else {
			this.showGlobPrompt()
		}
	}

	showAlreadyAddedFilesWarning(stagedFiles) {
		jumper.block('There are file(s) already staged for commit.')
		for (let file of stagedFiles) {
			jumper.block(chalk.red(file))
		}
		jumper.break()
		jumper.block(chalk.bold('These file(s) will be committed. Continue? (y/n) '), 'warning')

		jumper.render()
		jumper.jumpTo('warning', -1)
		keypress.beginInput()

		keypress.once('return', () => {
			let answer = keypress.input().toLowerCase()
			jumper.erase()

			if (['y', 'yes'].includes(answer)) {
				jumper.reset()
				this.showGlobPrompt()
			} else if (['n', 'no'].includes(answer)) {
				keypress.exit()
			}
		})
	}

	showGlobPrompt() {
		// configure keypress
		keypress.color(letter => chalk.red(letter))
		keypress.disableBehavior('tab right left')

		// show prompt and available files
		jumper.block(this.instructions, 'instructions')
		jumper.break()
		jumper.block(this.gitAddPrompt, 'enter')
		jumper.break()
		jumper.block(chalk.green('Files found:'), 'found')
		this.renderFoundGitFiles()

		jumper.jumpTo('enter', -1)
		keypress.beginInput()

		keypress.on('keypress', this.onType)
		keypress.on('tab', this.onTab)
		keypress.on('arrow', this.onArrow)

		keypress.once('return', () => {
			this.showCursor()
			keypress.removeListener('keypress', this.onType)
			keypress.removeListener('tab', this.onTab)
			this.renderAddSuccess()
			this.startCommit()
		})
	}

	onType(char, key) {
		if (['left', 'right', 'up', 'down', 'tab', 'return'].includes(key.name)) {
			return
		}

		if (this.showingIndicator) {
			this.showCursor()
			this.fileIndex = 0
			this.showingIndicator = false
		}

		let glob = keypress.input()
		let coloredInput = keypress.input(true)
		jumper.find('enter').content(this.gitAddPrompt + coloredInput)

		this.renderFoundGitFiles(glob)
		jumper.jumpTo('enter', -1)
	}

	onTab() {
		if (this.showingDiff) {
			return
		}

		this.showingDiff = true

		let file = this.showingIndicator ? this.getIndicatedFile() : keypress.input()
		let diff = gitDiffGlob(file, {
			caseSensitive: this.options.caseSensitive
		})
		let pagerExitFn = () => this.showingDiff = false

		pager(diff).then(pagerExitFn)
	}

	onArrow(dir) {
		if (['left', 'right'].includes(dir)) {
			return
		}
		if (!this.showingIndicator && dir === 'up') {
			return
		}

		let files = utils.getUniqueFilesOfTypes(this.options.find)
		let matches = utils.matchGlobsAgainstFiles(files, this.globs, {
			caseSensitive: this.options.caseSensitive
		})
		let numFiles = matches.length

		if (numFiles < 2) {
			return
		}

		// go to the correct file based on input direction
		if (!this.showingIndicator) {
			this.fileIndex = 0
			this.hideCursor()
		} else if (dir === 'up') {
			this.fileIndex -= 1
		} else if (dir === 'down') {
			this.fileIndex += 1
		}

		// return if the index is lower than 0 or greater than the number of files
		if (this.fileIndex < 0) {
			this.fileIndex = 0
			jumper.render()
			jumper.jumpTo('enter', -1)
			this.showCursor()
			this.showingIndicator = false
			return
		}
		if (this.fileIndex > numFiles - 1) {
			this.fileIndex = numFiles - 1
			return
		}

		// erase any previously printed indicators
		jumper.render()

		// write a file indicator on the current file line
		jumper.jumpTo(`gitFile${this.fileIndex}`, -1)
		process.stdout.write(ansiEscapes.cursorForward(2))
		process.stdout.write(chalk.bold.blue('⬅'))

		this.showingIndicator = true
	}

	showCursor() {
		process.stdout.write(ansiEscapes.cursorShow)
	}

	hideCursor() {
		process.stdout.write(ansiEscapes.cursorHide)
	}

	getIndicatedFile() {
		let currentLine = jumper.find(`gitFile${this.fileIndex}`).escapedText
		return currentLine.trim().split(' ')[1]
	}

	renderAddSuccess() {
		// if the indicator is showing, only add that specific file.
		// otherwise, add all files matching the given glob
		if (this.showingIndicator) {
			this.addFiles = [this.getIndicatedFile()]
		} else {
			let glob = keypress.input()
			this.addFiles = this.getGitFilesMatching(glob)
		}

		jumper.reset()

		jumper.block(chalk.green('Added files:'), 'found')
		for (let i = 0; i < this.addFiles.length; i++) {
			let file = this.addFiles[i]
			let id = `addedFile${i}`
			jumper.block(`${chalk.red(file)}  ${chalk.green('✓')}`, id)
		}
		jumper.break()
		jumper.block('---------------------------------------------')
		jumper.break()
		jumper.block(chalk.green('Enter commit message: "'), 'commit')
		jumper.render()

		jumper.jumpTo('commit', -1)
		process.stdout.write(chalk.green('"'))
		jumper.jumpTo('commit', -1)
	}

	startCommit() {
		keypress.color(letter => chalk.green(letter))
		keypress.beginInput()

		let onKeypress = () => {
			process.stdout.write(`${chalk.green('"')}`)
			process.stdout.write(ansiEscapes.cursorBackward(1))
		}
		keypress.on('keypress', onKeypress)

		keypress.once('return', () => {
			keypress.removeListener('keypress', onKeypress)
			this.commitMessage = keypress.input()

			let content = jumper.find('commit').text
			content = `${content}${keypress.input(true)}${chalk.green('"')}`
			jumper.find('commit').content(content)
			this.commit()
			this.complete()
		})

		keypress.removeListener('exit', this.exitBeforeAdd)
		keypress.on('exit', this.exitAfterAdd)
	}

	commit() {
		for (let file of this.addFiles) {
			execSync(`git add ${file}`)
		}
		execSync(`git commit -m "${this.commitMessage}"`)

		keypress.removeListener('exit', this.exitAfterAdd)
	}

	complete() {
		jumper.break()
		jumper.block('---------------------------------------------')
		jumper.break()
		jumper.block('Files committed successfully.')
		jumper.render()

		keypress.exit()
	}

	/**
	 * Finds all files that match the given glob and prints them to the screen,
	 * along with their git file type, e.g. 'staged', 'modified', etc.
	 * @param {string} [glob] - The glob to match against files.
	 */
	renderFoundGitFiles(glob = '*') {
		// remove previous search
		jumper.removeAllMatching(/gitFile\d+/)

		let fileMap = utils.getFilesOfTypes(this.options.find)
		let types = Object.keys(fileMap)
		let counter = 0

		for (let type of types) {
			let matches = utils.matchGlobsAgainstFiles(fileMap[type], glob, {
				caseSensitive: this.options.caseSensitive
			})

			for (let file of matches) {
				let text = `  ${chalk.bold.gray(`(${type})`)} ${chalk.red(file)}`
				jumper.block(text, `gitFile${counter}`)
				counter += 1
			}
		}

		jumper.render()
	}

	getGitFilesMatching(glob) {
		let files = utils.getUniqueFilesOfTypes(this.options.find)
		return utils.matchGlobsAgainstFiles(files, [glob], {
			caseSensitive: this.options.caseSensitive
		})
	}

	exitBeforeAdd() {
		jumper.erase()
	}

	exitAfterAdd() {
		process.stdout.write(ansiEscapes.cursorSavePosition)

		for (let i = 0; i < this.addFiles.length; i++) {
			let id = `addedFile${i}`
			jumper.jumpTo(id, -2)
			process.stdout.write(chalk.red('✗'))
		}

		process.stdout.write(ansiEscapes.cursorRestorePosition)
		console.log()
		console.log()
		console.log('Aborting -- no files added.')
	}
}

module.exports = Interactive
<commit_msg>Delete?<commit_after><|endoftext|>


<commit_before>/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import {
  Animated,
  TouchableWithoutFeedback,
  FlatList,
  StyleSheet,
  View
} from 'react-native';
import { Text } from './Text';
import { Icon } from './Icon';
import variable from '../theme/variables/platform';

const styles = StyleSheet.create({
  defaultHeader: {
    flexDirection: 'row',
    padding: variable.accordionContentPadding,
    justifyContent: 'space-between',
    alignItems: 'center'
  }
});

class DefaultHeader extends React.Component {
  render() {
    const {
      disable,
      expanded,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      title
    } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <View
        style={[
          styles.defaultHeader,
          headerStyle || { backgroundColor: variables.headerStyle }
        ]}
      >
        <Text style={{ color: disable ? variable.disableRow : null }}>
          {' '}
          {title}
        </Text>
        <Icon
          style={[
            { fontSize: variables.accordionIconFontSize },
            expanded
              ? expandedIcon && expandedIconStyle
                ? expandedIconStyle
                : { color: variables.expandedIconStyle }
              : icon && iconStyle
              ? iconStyle
              : { color: disable ? variable.disableRow : variables.iconStyle }
          ]}
          name={
            expanded ? expandedIcon || 'ios-arrow-up' : icon || 'ios-arrow-down'
          }
        />
      </View>
    );
  }
}

class DefaultContent extends React.Component {
  render() {
    const { content, contentStyle } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <Text
        style={[
          { padding: variable.accordionContentPadding },
          contentStyle || { backgroundColor: variables.contentStyle }
        ]}
      >
        {content}
      </Text>
    );
  }
}

class AccordionSubItem extends React.Component {
  state = {
    fadeAnim: new Animated.Value(0.3)
  };
  componentDidMount() {
    Animated.timing(this.state.fadeAnim, {
      toValue: 1,
      duration: 500,
      useNativeDriver: true
    }).start();
  }
  render() {
    const { children, style } = this.props;
    const { fadeAnim } = this.state;
    return (
      <Animated.View style={{ ...style, opacity: fadeAnim }}>
        {children}
      </Animated.View>
    );
  }
}

class AccordionItem extends React.Component {
  render() {
    const {
      contentStyle,
      disable,
      expanded,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      index,
      item,
      onAccordionClose,
      onAccordionOpen,
      renderContent,
      renderHeader,
      setSelected
    } = this.props;
    return (
      <View>
        <TouchableWithoutFeedback
          disabled={disable}
          onPress={() => {
            onAccordionOpen && !expanded && onAccordionOpen(item, index);
            onAccordionClose && expanded && onAccordionClose(item, index);
            setSelected(index);
          }}
        >
          <View>
            {renderHeader ? (
              renderHeader(item, expanded, index)
            ) : (
              <DefaultHeader
                disable={disable}
                expanded={expanded}
                headerStyle={headerStyle}
                icon={icon}
                iconStyle={iconStyle}
                expandedIcon={expandedIcon}
                expandedIconStyle={expandedIconStyle}
              />
            )}
          </View>
        </TouchableWithoutFeedback>
        {expanded ? (
          <AccordionSubItem>
            {renderContent ? (
              renderContent(item, index)
            ) : (
              <DefaultContent
                content={item.content}
                contentStyle={contentStyle}
              />
            )}
          </AccordionSubItem>
        ) : null}
      </View>
    );
  }
}

export class Accordion extends React.Component {
  constructor(props) {
    super(props);

    const { expanded, expandMultiple } = this.props;
    // eslint-disable-next-line no-unused-vars
    let selected;
    if (expanded !== undefined && expanded !== null) {
      selected = Array.isArray(expanded) ? expanded : [expanded];
      selected = expandMultiple ? selected : selected.slice(0, 1);
    }
    this.state = {
      selected: props.expanded
    };
  }

  setSelected(index) {
    const { expandMultiple } = this.props;
    const selected = this.state.selected.slice();
    if (selected.indexOf(index) !== -1) {
      selected.splice(selected.indexOf(index), 1);
      this.setState({ selected });
    } else {
      selected.push(index);
      this.setState({ selected: expandMultiple ? selected : [index] });
    }
  }

  render() {
    const {
      contentStyle,
      dataArray,
      disable,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      onAccordionClose,
      onAccordionOpen,
      renderContent,
      renderHeader,
      style
    } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <FlatList
        data={dataArray}
        extraData={this.state}
        style={[
          {
            borderColor: variables.accordionBorderColor,
            borderWidth: variables.borderWidth
          },
          style
        ]}
        keyExtractor={(item, index) => String(index)}
        renderItem={({ item, index }) => (
          <AccordionItem
            disable={disable === index}
            key={String(index)}
            item={item}
            expanded={this.state.selected.indexOf(index) !== -1}
            index={index}
            setSelected={i => this.setSelected(i)}
            headerStyle={headerStyle}
            contentStyle={contentStyle}
            renderHeader={renderHeader}
            renderContent={renderContent}
            icon={icon}
            iconStyle={iconStyle}
            expandedIcon={expandedIcon}
            expandedIconStyle={expandedIconStyle}
            onAccordionOpen={onAccordionOpen}
            onAccordionClose={onAccordionClose}
          />
        )}
        {...this.props}
      />
    );
  }
}
<commit_msg>added title to accordian<commit_after>/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import {
  Animated,
  TouchableWithoutFeedback,
  FlatList,
  StyleSheet,
  View
} from 'react-native';
import { Text } from './Text';
import { Icon } from './Icon';
import variable from '../theme/variables/platform';

const styles = StyleSheet.create({
  defaultHeader: {
    flexDirection: 'row',
    padding: variable.accordionContentPadding,
    justifyContent: 'space-between',
    alignItems: 'center'
  }
});

class DefaultHeader extends React.Component {
  render() {
    const {
      disable,
      expanded,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      title
    } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <View
        style={[
          styles.defaultHeader,
          headerStyle || { backgroundColor: variables.headerStyle }
        ]}
      >
        <Text style={{ color: disable ? variable.disableRow : null }}>
          {' '}
          {title}
        </Text>
        <Icon
          style={[
            { fontSize: variables.accordionIconFontSize },
            expanded
              ? expandedIcon && expandedIconStyle
                ? expandedIconStyle
                : { color: variables.expandedIconStyle }
              : icon && iconStyle
              ? iconStyle
              : { color: disable ? variable.disableRow : variables.iconStyle }
          ]}
          name={
            expanded ? expandedIcon || 'ios-arrow-up' : icon || 'ios-arrow-down'
          }
        />
      </View>
    );
  }
}

class DefaultContent extends React.Component {
  render() {
    const { content, contentStyle } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <Text
        style={[
          { padding: variable.accordionContentPadding },
          contentStyle || { backgroundColor: variables.contentStyle }
        ]}
      >
        {content}
      </Text>
    );
  }
}

class AccordionSubItem extends React.Component {
  state = {
    fadeAnim: new Animated.Value(0.3)
  };
  componentDidMount() {
    Animated.timing(this.state.fadeAnim, {
      toValue: 1,
      duration: 500,
      useNativeDriver: true
    }).start();
  }
  render() {
    const { children, style } = this.props;
    const { fadeAnim } = this.state;
    return (
      <Animated.View style={{ ...style, opacity: fadeAnim }}>
        {children}
      </Animated.View>
    );
  }
}

class AccordionItem extends React.Component {
  render() {
    const {
      contentStyle,
      disable,
      expanded,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      index,
      item,
      onAccordionClose,
      onAccordionOpen,
      renderContent,
      renderHeader,
      setSelected
    } = this.props;
    return (
      <View>
        <TouchableWithoutFeedback
          disabled={disable}
          onPress={() => {
            onAccordionOpen && !expanded && onAccordionOpen(item, index);
            onAccordionClose && expanded && onAccordionClose(item, index);
            setSelected(index);
          }}
        >
          <View>
            {renderHeader ? (
              renderHeader(item, expanded, index)
            ) : (
              <DefaultHeader
                title={item.title}
                disable={disable}
                expanded={expanded}
                headerStyle={headerStyle}
                icon={icon}
                iconStyle={iconStyle}
                expandedIcon={expandedIcon}
                expandedIconStyle={expandedIconStyle}
              />
            )}
          </View>
        </TouchableWithoutFeedback>
        {expanded ? (
          <AccordionSubItem>
            {renderContent ? (
              renderContent(item, index)
            ) : (
              <DefaultContent
                content={item.content}
                contentStyle={contentStyle}
              />
            )}
          </AccordionSubItem>
        ) : null}
      </View>
    );
  }
}

export class Accordion extends React.Component {
  constructor(props) {
    super(props);

    const { expanded, expandMultiple } = this.props;
    // eslint-disable-next-line no-unused-vars
    let selected;
    if (expanded !== undefined && expanded !== null) {
      selected = Array.isArray(expanded) ? expanded : [expanded];
      selected = expandMultiple ? selected : selected.slice(0, 1);
    }
    this.state = {
      selected: props.expanded
    };
  }

  setSelected(index) {
    const { expandMultiple } = this.props;
    const selected = this.state.selected.slice();
    if (selected.indexOf(index) !== -1) {
      selected.splice(selected.indexOf(index), 1);
      this.setState({ selected });
    } else {
      selected.push(index);
      this.setState({ selected: expandMultiple ? selected : [index] });
    }
  }

  render() {
    const {
      contentStyle,
      dataArray,
      disable,
      expandedIcon,
      expandedIconStyle,
      headerStyle,
      icon,
      iconStyle,
      onAccordionClose,
      onAccordionOpen,
      renderContent,
      renderHeader,
      style
    } = this.props;
    const variables = this.context.theme
      ? this.context.theme['@@shoutem.theme/themeStyle'].variables
      : variable;
    return (
      <FlatList
        data={dataArray}
        extraData={this.state}
        style={[
          {
            borderColor: variables.accordionBorderColor,
            borderWidth: variables.borderWidth
          },
          style
        ]}
        keyExtractor={(item, index) => String(index)}
        renderItem={({ item, index }) => (
          <AccordionItem
            disable={disable === index}
            key={String(index)}
            item={item}
            expanded={this.state.selected.indexOf(index) !== -1}
            index={index}
            setSelected={i => this.setSelected(i)}
            headerStyle={headerStyle}
            contentStyle={contentStyle}
            renderHeader={renderHeader}
            renderContent={renderContent}
            icon={icon}
            iconStyle={iconStyle}
            expandedIcon={expandedIcon}
            expandedIconStyle={expandedIconStyle}
            onAccordionOpen={onAccordionOpen}
            onAccordionClose={onAccordionClose}
          />
        )}
        {...this.props}
      />
    );
  }
}
<|endoftext|>


<commit_before>/*©agpl*************************************************************************
*                                                                              *
* This file is part of FRIEND UNIFYING PLATFORM.                               *
* Copyright (c) Friend Software Labs AS. All rights reserved.                  *
*                                                                              *
* Licensed under the Source EULA. Please refer to the copy of the GNU Affero   *
* General Public License, found in the file license_agpl.txt.                  *
*                                                                              *
*****************************************************************************©*/

var AdminViews = {};

Application.run = function( msg )
{

	this.sendMessage( { type: 'system', command: 'setsingleinstance', value: true } );

	var v = new View( {
		title: 'Friend Admin',
		width: 1280,
		height: 960
	} );

	this.mainView = v;

	var f = new File( 'Progdir:Templates/main.html' );
	f.onLoad = function( data )
	{
		v.setContent( data );
	}
	f.load();

	v.onClose = function()
	{
		Application.quit();
	}

	v.setMenuItems( [
		{
			name: i18n( 'i18n_file' ),
			items: [
				{
					name: i18n( 'i18n_about' ),
					command: 'about'
				}
			]
		}
	] );

}

// Available functions
messageFunctions = {

	about( msg )
	{
		console.log( msg );

		if( !AdminViews[ 'about' ] )
		{

			var d = new File( 'Progdir:Config.conf' );
			d.onLoad = function( conf )
			{
				var obj = {
					title: 'About Friend Admin',
					width: 400,
					height: 288
				};

				var f = new File( 'Progdir:Templates/about.html' );

				try
				{
					var json = JSON.parse( conf );

					console.log( json );

					obj.title += ( ' (v' + json.Version + ')' );

					f.replacements = { content: /*JSON.stringify( */conf/*, null, 2 )*/ };
				}
				catch( e ){  }

				AdminViews[ 'about' ] = new View( obj );

				f.onLoad = function( data )
				{
					AdminViews[ 'about' ].setContent( data );
				}
				f.load();

				AdminViews[ 'about' ].onClose = function()
				{
					AdminViews[ 'about' ] = false;
				}

			}
			d.load();

		}

		return Application.mainView.sendMessage( {
			command: 'about'
		} );
	}

};

// Execute on received message
Application.receiveMessage = function( msg )
{
	if( !msg.command ) return;
	if( messageFunctions[ msg.command ] )
	{
		return messageFunctions[ msg.command ]( msg );
	}
	return false;
}

<commit_msg>Version number from config next to name of app.<commit_after>/*©agpl*************************************************************************
*                                                                              *
* This file is part of FRIEND UNIFYING PLATFORM.                               *
* Copyright (c) Friend Software Labs AS. All rights reserved.                  *
*                                                                              *
* Licensed under the Source EULA. Please refer to the copy of the GNU Affero   *
* General Public License, found in the file license_agpl.txt.                  *
*                                                                              *
*****************************************************************************©*/

var AdminViews = {};

var Config = { data: {}, raw: '' };

Application.run = function( msg )
{

	this.sendMessage( { type: 'system', command: 'setsingleinstance', value: true } );

	var d = new File( 'Progdir:Config.conf' );
	d.onLoad = function( conf )
	{

		try
		{
			Config.raw  = conf;
			Config.data = JSON.parse( conf );

			console.log( Config.data );
		}
		catch( e ){  }

		var v = new View( {
			title: 'Friend Admin' + ( Config.data.Version ? ' (v' + Config.data.Version + ')' : '' ),
			width: 1280,
			height: 960
		} );

		this.mainView = v;

		var f = new File( 'Progdir:Templates/main.html' );
		f.onLoad = function( data )
		{
			v.setContent( data );
		}
		f.load();

		v.onClose = function()
		{
			Application.quit();
		}

		v.setMenuItems( [
			{
				name: i18n( 'i18n_file' ),
				items: [
					{
						name: i18n( 'i18n_about' ),
						command: 'about'
					}
				]
			}
		] );

	}
	d.load();

}

// Available functions
messageFunctions = {

	about( msg )
	{
		//console.log( msg );

		if( !AdminViews[ 'about' ] )
		{

			AdminViews[ 'about' ] = new View( {
				title: 'About Friend Admin' + ( Config.data.Version ? ' (v' + Config.data.Version + ')' : '' ),
				width: 400,
				height: 288
			} );

			var f = new File( 'Progdir:Templates/about.html' );
			f.replacements = {
				content: Config.raw
			};
			f.onLoad = function( data )
			{
				AdminViews[ 'about' ].setContent( data );
			}
			f.load();

			AdminViews[ 'about' ].onClose = function()
			{
				AdminViews[ 'about' ] = false;
			}

		}

		//return Application.mainView.sendMessage( {
		//	command: 'about'
		//} );
	}

};

// Execute on received message
Application.receiveMessage = function( msg )
{
	if( !msg.command ) return;
	if( messageFunctions[ msg.command ] )
	{
		return messageFunctions[ msg.command ]( msg );
	}
	return false;
}

<|endoftext|>


<commit_before>/**
 * This example script expects a JSON blob generated by react-docgen as input,
 * e.g. react-docgen components/* | buildDocs.sh
 */
const File = require('vinyl');
const through = require('through2');

const docgen = require('../plugins/docgen');
const paths = require('../paths.json');

/**
 * Copyright (c) 2015, 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.
 */

const stringOfLength = (string, length) => {
    let newString = '';
    for (let i = 0; i < length; i++) { // eslint-disable-line id-length
        newString += string;
    }
    return newString;
};

const generateTitle = (name) => {
    const title = `\`${name}\` (component)`;
    return `${title}\n${stringOfLength('=', title.length)}\n`;
};

const generateDescription = (description) => `${description}\n`;

const generatePropType = (type) => {
    let values;
    if (Array.isArray(type.value)) {
        values = `(${type.value.map((typeValue) => typeValue.name || typeValue.value).join('|')})`;
    } else {
        values = type.value;
    }

    return `type: \`${type.name}${values || ''}\`\n`;
};

const generatePropDefaultValue = (value) => `defaultValue: \`${value.value}\`\n`;

const generateProp = (propName, prop) => {
    const descriptor = `${propName}${prop.required ? ' (required)' : ''}`;

    const description = prop.description ? `${prop.description}\n\n` : '';

    const type = prop.type ? generatePropType(prop.type) : '';

    const def = prop.defaultValue ? generatePropDefaultValue(prop.defaultValue) : '';

    return `### ${descriptor}\n\n${description}${type}${def}\n`;
};

const generateProps = (props) => {
    const description = Object.keys(props)
        .sort()
        .map((propName) => generateProp(propName, props[propName]))
        .join('\n');
    return `Props\n-----\n\n${description}`;
};

const generateMarkdown = function generateMarkdown(name, reactAPI) {

    const markdownString =
        `${generateTitle(name)}\n`
        + `${generateDescription(reactAPI.description)}\n`
        + `${generateProps(reactAPI.props)}`;

    return markdownString;
};

const toMarkdown = function toMarkdown(options = {}) {
    // implement the default options
    //
    const opts = Object.assign({
        name: 'react-docgen.md',
    }, options);

    return through.obj(function write(file, encoding, callback) {
        if (file.isNull()) {
            return callback(null, file);
        }

        const reactAPI = JSON.parse(file.contents.toString());

        let markdown = '';

        Object.keys(reactAPI).forEach((filename) => {
            markdown += generateMarkdown(filename, reactAPI[filename]);
        });

        // push the new file containing the JSON-encoded results
        //
        this.push(new File({
            contents: new Buffer(markdown),
            base:     '/tmp',
            path:     `/tmp/${opts.name}`,
        }));

        // carry on...
        //
        return callback();

    });
};

module.exports = (gulp) =>
    gulp.src(paths.source)
        .pipe(docgen())
        .pipe(toMarkdown({ name: 'REACT.md' }))
        .pipe(gulp.dest('./'));
<commit_msg>moved markdown plugin code to separate file<commit_after>
const docgen = require('../plugins/docgen');
const markdown = require('../plugins/markdown');
const paths = require('../paths.json');

module.exports = (gulp) =>
    gulp.src(paths.source)
        .pipe(docgen())
        .pipe(markdown({ name: 'REACT.md' }))
        .pipe(gulp.dest('./'));
<|endoftext|>


<commit_before>	// elenco tracce
	var image_array = new Array();

	image_array[0] = ['https://goo.gl/XeJumj', 'https://goo.gl/eLnsbF', 'https://goo.gl/VzM8Ge', 'https://goo.gl/To2T9k', 'https://goo.gl/hqaq9P', 'https://goo.gl/m6TCHh',
        'https://goo.gl/RnRTPf', 'https://goo.gl/5y1Tav', 'https://goo.gl/43BomX', 'https://goo.gl/eQvU5F', 'https://goo.gl/dKeMxe', 'https://goo.gl/2cnyaG',
        'https://goo.gl/F5RQmM', 'https://goo.gl/qZQGDx', 'https://goo.gl/bmPbWo', 'https://goo.gl/dQSs7L', 'https://goo.gl/tEXg5a', 'https://goo.gl/NSfN2H',
        'https://goo.gl/ep2fVM', 'https://goo.gl/qv5Kw1', 'https://goo.gl/hmxnMs', 'https://goo.gl/7bw4pK', 'https://goo.gl/aP5ZoK','https://goo.gl/y9Nskt',
        'https://goo.gl/Vf16i3', 'https://goo.gl/ZYhmPn', 'https://goo.gl/aekTxR', 'https://goo.gl/TwYqtX',  'https://goo.gl/76saNX'];

	image_array[1] = [];

	image_array[2] = ['https://goo.gl/9yr6pq', 'https://goo.gl/FxuwG9', 'https://goo.gl/eLGUpg', 'https://goo.gl/i1EKkz', 'https://goo.gl/uJbRYb', 'https://goo.gl/Fz12u4', 'https://goo.gl/27MQkV',
	  'https://goo.gl/UkquXv', 'https://goo.gl/HckgJr', 'https://goo.gl/o9PG41', 'https://goo.gl/FALZjw'];

	image_array[3] = ['https://goo.gl/ZstqLH', 'https://goo.gl/oHrGXS', 'https://goo.gl/ywdStp', 'https://goo.gl/jYQiEV', 'https://goo.gl/4Lc2aP', 'https://goo.gl/oHHNLb',
	  'https://goo.gl/HQJwe4', 'https://goo.gl/tRjEq3', 'https://goo.gl/xQErJi', 'https://goo.gl/Fzi1K8', 'https://goo.gl/2DZyAV', 'https://goo.gl/N4kh99',
	  'https://goo.gl/mu3fSd', 'https://goo.gl/9HGWNJ', 'https://goo.gl/Nm7394', 'https://goo.gl/h3ukof', 'https://goo.gl/3RQNJQ', 'https://goo.gl/cvwJsR', 'https://goo.gl/gkmDnS',
	  'https://goo.gl/ifFWLx', 'https://goo.gl/aErbjV'];

	image_array[4] = ['https://goo.gl/qr7NHc', 'https://goo.gl/DS7DTk',
	'https://goo.gl/so5X6n', 'https://goo.gl/bF92zp', 'https://goo.gl/qVMvN3',
	  'https://goo.gl/B1esB8', 'https://goo.gl/9Hig6J', 'https://goo.gl/TWPmej', 'https://goo.gl/E3xaU2', 'https://goo.gl/py39n2', 'https://goo.gl/VChtT1',
	  'https://goo.gl/nj8Eqw', 'https://goo.gl/pdZrRD', 'https://goo.gl/YHhmHU', 'https://goo.gl/1d8vBA', 'https://goo.gl/3kc6qQ', 'https://goo.gl/78PZ5c',
	  'https://goo.gl/B3ZFvS', 'https://goo.gl/NsyL1A', 'https://goo.gl/D4PP1J', 'https://goo.gl/tv9m69'];

	image_array[5] = ['https://goo.gl/WBSemk', 'https://goo.gl/9kthxA', 'https://goo.gl/AzszdW', 'https://goo.gl/zfjxxB', 'https://goo.gl/FhuQKZ',
	  'https://goo.gl/zF1dRW', 'https://goo.gl/EAZTF6', 'https://goo.gl/dQWAFx', 'https://goo.gl/BbvoBH', 'https://goo.gl/WPqWNp',
	  'https://goo.gl/bwcqjg', 'https://goo.gl/eMQc84', 'https://goo.gl/3fepPd', 'https://goo.gl/dPSZ35', 'https://goo.gl/A7YanG'];

	image_array[6] = ['https://goo.gl/e1GKPf', 'https://goo.gl/WwLVBq', 'https://goo.gl/gjrsLH', 'https://goo.gl/EhuZqG',
	  'https://goo.gl/UkSfrn', 'https://goo.gl/BskPAf',
	  'https://goo.gl/MyfQY5', 'https://goo.gl/C7buVe', 'https://goo.gl/j76Lz3', 'https://goo.gl/jeUvWa', 'https://goo.gl/SB1YH4',
	  'https://goo.gl/yjdy5m', 'https://goo.gl/56Kwky', 'https://goo.gl/eu4cbm', 'https://goo.gl/AvCBqJ', 'https://goo.gl/DcBENE',
	  'https://goo.gl/2jHLa7', 'https://goo.gl/H3o8AB', 'https://goo.gl/XQ1bRT', 'https://goo.gl/b3gaht', 'https://goo.gl/3nX28a',
	  'https://goo.gl/Zeh1Zy', 'https://goo.gl/22udYX', 'https://goo.gl/YB4kmC', 'https://goo.gl/qp4dK1', 'https://goo.gl/4TFpid',
	  'https://goo.gl/yGgzGq'];

	image_array[7] = ['https://goo.gl/d4M5cW', 'https://goo.gl/WjevMT', 'https://goo.gl/M8Kr3R', 'https://goo.gl/cF7rAk'];

	image_array[8] = ['https://goo.gl/Lg8noM', 'https://goo.gl/KJSCJf', 'https://goo.gl/PcLMr3', 'https://goo.gl/wtr8gD'];

	image_array[9] = [];

	image_array[10] = ['https://goo.gl/Ye46cY', 'https://goo.gl/Mm67Wd', 'https://goo.gl/3k4wM5', 'https://goo.gl/dCPLRa',
	  'https://goo.gl/GMZ7oa', 'https://goo.gl/PAuNV8'];

	image_array[11] = [];

	image_array[12] = ['https://goo.gl/rAQBbW', 'https://goo.gl/GQ7MRa', 'https://goo.gl/EDcK3B', 'https://goo.gl/dHhPUm', 'https://goo.gl/ZQL4GK',
	  'https://goo.gl/rF4sFD', 'https://goo.gl/6EoepX', 'https://goo.gl/yxLEFm', 'https://goo.gl/sUiBdS', 'https://goo.gl/xDZYzW',
	  'https://goo.gl/bxMT6m', 'https://goo.gl/SCH9Ap', 'https://goo.gl/4UY5Vu'];

	image_array[13] = ['https://goo.gl/Pgp7eq', 'https://goo.gl/f8mxZq', 'https://goo.gl/SG4STW', 'https://goo.gl/86EBRp', 'https://goo.gl/oVHP8c',
	  'https://goo.gl/xocvWW', 'https://goo.gl/6SJRpn', 'https://goo.gl/wuqijA', 'https://goo.gl/8nSD3c', 'https://goo.gl/YLhiuU', 'https://goo.gl/xtfJTW',
	  'https://goo.gl/tqyvnU', 'https://goo.gl/tBYg7L', 'https://goo.gl/74JMdw', 'https://goo.gl/VxPpNg', 'https://goo.gl/GfJCiN', 'https://goo.gl/Unk3iy',
	  'https://goo.gl/eeRxYx', 'https://goo.gl/88tai4', 'https://goo.gl/qbYySS', 'https://goo.gl/kXo8zn', 'https://goo.gl/Vg5JBh', 'https://goo.gl/G4vJy2',
	  'https://goo.gl/Sw8g8C', 'https://goo.gl/Uxj5Rp', 'https://goo.gl/sXAaLT', 'https://goo.gl/Ejnf6o', 'https://goo.gl/VCNX7b', 'https://goo.gl/wxrqAe',
	  'https://goo.gl/LEBXZh', 'https://goo.gl/UfaSRa', 'https://goo.gl/DJtK9j', 'https://goo.gl/iUKfJD', 'https://goo.gl/AmGqPj',  'https://goo.gl/Vp3A7d'];

	image_array[14] = ['https://goo.gl/ktgq3Q', 'https://goo.gl/hPTxB1', 'https://goo.gl/ZYfb2r', 'https://goo.gl/wVtdyt', 'https://goo.gl/2RtC4b',
	  'https://goo.gl/m8hDz6', 'https://goo.gl/gqfCNh', 'https://goo.gl/cNvyEg', 'https://goo.gl/BB4qrG', 'https://goo.gl/jqDHTM', 'https://goo.gl/t9UsSX',
	  'https://goo.gl/4Ecx8j', 'https://goo.gl/eAusF6', 'https://goo.gl/5YkGHs', 'https://goo.gl/61PTwY', 'https://goo.gl/DJqVHt', 'https://goo.gl/Efiuzf'];

	image_array[15] = ['https://goo.gl/tX4pvA', 'https://goo.gl/QdGFv1', 'https://goo.gl/EXzuuy', 'https://goo.gl/sp4zpL', 'https://goo.gl/gBGwKN',
	  'https://goo.gl/AuKXcq', 'https://goo.gl/boVvig', 'https://goo.gl/sJJZHg', 'https://goo.gl/h4oHjD', 'https://goo.gl/cWY2Ro', 'https://goo.gl/zQzBVj',
	  'https://goo.gl/VuuNL6'];

	image_array[16] = ['https://goo.gl/4UXECa', 'https://goo.gl/AWKAgh', 'https://goo.gl/j2kvRE', 'https://goo.gl/8wP9TS', 'https://goo.gl/gTqe4e',
	'https://goo.gl/a3Raq6', 'https://goo.gl/i8aMeK', 'https://goo.gl/WRwG1T','https://goo.gl/kmgLBz', 'https://goo.gl/rxsPEy', 'https://goo.gl/vQ4EPp',
	'https://goo.gl/QqtnRu', 'https://www.youtube.com/embed/w1XULu9uCwM', 'https://www.youtube.com/embed/ifQF8XGVY6A', 'https://goo.gl/eYdYk6',
	'https://goo.gl/b1Vayo', 'https://goo.gl/iAepPx', 'https://goo.gl/C4RrAJ', 'https://goo.gl/Gfb2nB', 'https://goo.gl/4wrwjh', 'https://goo.gl/4brv9r'];

	image_array[17] = ['https://goo.gl/NhA5q9', 'https://goo.gl/kmgiin', 'https://goo.gl/qCz8gD', 'https://goo.gl/bvdp4U', 'https://goo.gl/Q8YDD8', 'https://goo.gl/GbkF2U',
        'https://goo.gl/udwb2U', 'https://goo.gl/MWMfHd', 'https://goo.gl/PhiBjY', 'https://goo.gl/9GrY8F', 'https://goo.gl/WGmUnc'];
<commit_msg>Aggiunti i filmati della via del polleggio<commit_after>	// elenco tracce
	var image_array = new Array();

	image_array[0] = ['https://goo.gl/XeJumj', 'https://goo.gl/eLnsbF', 'https://goo.gl/VzM8Ge', 'https://goo.gl/To2T9k', 'https://goo.gl/hqaq9P', 'https://goo.gl/m6TCHh',
        'https://goo.gl/RnRTPf', 'https://goo.gl/5y1Tav', 'https://goo.gl/43BomX', 'https://goo.gl/eQvU5F', 'https://goo.gl/dKeMxe', 'https://goo.gl/2cnyaG',
        'https://goo.gl/F5RQmM', 'https://goo.gl/qZQGDx', 'https://goo.gl/bmPbWo', 'https://goo.gl/dQSs7L', 'https://goo.gl/tEXg5a', 'https://goo.gl/NSfN2H',
        'https://goo.gl/ep2fVM', 'https://goo.gl/qv5Kw1', 'https://goo.gl/hmxnMs', 'https://goo.gl/7bw4pK', 'https://goo.gl/aP5ZoK','https://goo.gl/y9Nskt',
        'https://goo.gl/Vf16i3', 'https://goo.gl/ZYhmPn', 'https://goo.gl/aekTxR', 'https://goo.gl/TwYqtX',  'https://goo.gl/76saNX'];

	image_array[1] = [];

	image_array[2] = ['https://goo.gl/9yr6pq', 'https://goo.gl/FxuwG9', 'https://goo.gl/eLGUpg', 'https://goo.gl/i1EKkz', 'https://goo.gl/uJbRYb', 'https://goo.gl/Fz12u4', 'https://goo.gl/27MQkV',
	  'https://goo.gl/UkquXv', 'https://goo.gl/HckgJr', 'https://goo.gl/o9PG41', 'https://goo.gl/FALZjw'];

	image_array[3] = ['https://goo.gl/ZstqLH', 'https://goo.gl/oHrGXS', 'https://goo.gl/ywdStp', 'https://goo.gl/jYQiEV', 'https://goo.gl/4Lc2aP', 'https://goo.gl/oHHNLb',
	  'https://goo.gl/HQJwe4', 'https://goo.gl/tRjEq3', 'https://goo.gl/xQErJi', 'https://goo.gl/Fzi1K8', 'https://goo.gl/2DZyAV', 'https://goo.gl/N4kh99',
	  'https://goo.gl/mu3fSd', 'https://goo.gl/9HGWNJ', 'https://goo.gl/Nm7394', 'https://goo.gl/h3ukof', 'https://goo.gl/3RQNJQ', 'https://goo.gl/cvwJsR', 'https://goo.gl/gkmDnS',
	  'https://goo.gl/ifFWLx', 'https://goo.gl/aErbjV'];

	image_array[4] = ['https://goo.gl/qr7NHc', 'https://goo.gl/DS7DTk',
	'https://goo.gl/so5X6n', 'https://goo.gl/bF92zp', 'https://goo.gl/qVMvN3',
	  'https://goo.gl/B1esB8', 'https://goo.gl/9Hig6J', 'https://goo.gl/TWPmej', 'https://goo.gl/E3xaU2', 'https://goo.gl/py39n2', 'https://goo.gl/VChtT1',
	  'https://goo.gl/nj8Eqw', 'https://goo.gl/pdZrRD', 'https://goo.gl/YHhmHU', 'https://goo.gl/1d8vBA', 'https://goo.gl/3kc6qQ', 'https://goo.gl/78PZ5c',
	  'https://goo.gl/B3ZFvS', 'https://goo.gl/NsyL1A', 'https://goo.gl/D4PP1J', 'https://goo.gl/tv9m69'];

	image_array[5] = ['https://goo.gl/WBSemk', 'https://goo.gl/9kthxA', 'https://goo.gl/AzszdW', 'https://goo.gl/zfjxxB', 'https://goo.gl/FhuQKZ',
	  'https://goo.gl/zF1dRW', 'https://goo.gl/EAZTF6', 'https://goo.gl/dQWAFx', 'https://goo.gl/BbvoBH', 'https://goo.gl/WPqWNp',
	  'https://goo.gl/bwcqjg', 'https://goo.gl/eMQc84', 'https://goo.gl/3fepPd', 'https://goo.gl/dPSZ35', 'https://goo.gl/A7YanG'];

	image_array[6] = ['https://goo.gl/e1GKPf', 'https://goo.gl/WwLVBq', 'https://goo.gl/gjrsLH', 'https://goo.gl/EhuZqG',
	  'https://goo.gl/UkSfrn', 'https://goo.gl/BskPAf',
	  'https://goo.gl/MyfQY5', 'https://goo.gl/C7buVe', 'https://goo.gl/j76Lz3', 'https://goo.gl/jeUvWa', 'https://goo.gl/SB1YH4',
	  'https://www.youtube.com/embed/j-v0kOzsrS4', 'https://www.youtube.com/embed/fCK0GBSFtgM',
	  'https://goo.gl/yjdy5m', 'https://goo.gl/56Kwky', 'https://goo.gl/eu4cbm', 'https://goo.gl/AvCBqJ', 'https://goo.gl/DcBENE',
	  'https://goo.gl/2jHLa7', 'https://goo.gl/H3o8AB', 'https://goo.gl/XQ1bRT', 'https://goo.gl/b3gaht', 'https://goo.gl/3nX28a',
	  'https://goo.gl/Zeh1Zy', 'https://goo.gl/22udYX', 'https://goo.gl/YB4kmC', 'https://goo.gl/qp4dK1', 'https://goo.gl/4TFpid',
	  'https://goo.gl/yGgzGq'];

	image_array[7] = ['https://goo.gl/d4M5cW', 'https://goo.gl/WjevMT', 'https://goo.gl/M8Kr3R', 'https://goo.gl/cF7rAk'];

	image_array[8] = ['https://goo.gl/Lg8noM', 'https://goo.gl/KJSCJf', 'https://goo.gl/PcLMr3', 'https://goo.gl/wtr8gD'];

	image_array[9] = [];

	image_array[10] = ['https://goo.gl/Ye46cY', 'https://goo.gl/Mm67Wd', 'https://goo.gl/3k4wM5', 'https://goo.gl/dCPLRa',
	  'https://goo.gl/GMZ7oa', 'https://goo.gl/PAuNV8'];

	image_array[11] = [];

	image_array[12] = ['https://goo.gl/rAQBbW', 'https://goo.gl/GQ7MRa', 'https://goo.gl/EDcK3B', 'https://goo.gl/dHhPUm', 'https://goo.gl/ZQL4GK',
	  'https://goo.gl/rF4sFD', 'https://goo.gl/6EoepX', 'https://goo.gl/yxLEFm', 'https://goo.gl/sUiBdS', 'https://goo.gl/xDZYzW',
	  'https://goo.gl/bxMT6m', 'https://goo.gl/SCH9Ap', 'https://goo.gl/4UY5Vu'];

	image_array[13] = ['https://goo.gl/Pgp7eq', 'https://goo.gl/f8mxZq', 'https://goo.gl/SG4STW', 'https://goo.gl/86EBRp', 'https://goo.gl/oVHP8c',
	  'https://goo.gl/xocvWW', 'https://goo.gl/6SJRpn', 'https://goo.gl/wuqijA', 'https://goo.gl/8nSD3c', 'https://goo.gl/YLhiuU', 'https://goo.gl/xtfJTW',
	  'https://goo.gl/tqyvnU', 'https://goo.gl/tBYg7L', 'https://goo.gl/74JMdw', 'https://goo.gl/VxPpNg', 'https://goo.gl/GfJCiN', 'https://goo.gl/Unk3iy',
	  'https://goo.gl/eeRxYx', 'https://goo.gl/88tai4', 'https://goo.gl/qbYySS', 'https://goo.gl/kXo8zn', 'https://goo.gl/Vg5JBh', 'https://goo.gl/G4vJy2',
	  'https://goo.gl/Sw8g8C', 'https://goo.gl/Uxj5Rp', 'https://goo.gl/sXAaLT', 'https://goo.gl/Ejnf6o', 'https://goo.gl/VCNX7b', 'https://goo.gl/wxrqAe',
	  'https://goo.gl/LEBXZh', 'https://goo.gl/UfaSRa', 'https://goo.gl/DJtK9j', 'https://goo.gl/iUKfJD', 'https://goo.gl/AmGqPj',  'https://goo.gl/Vp3A7d'];

	image_array[14] = ['https://goo.gl/ktgq3Q', 'https://goo.gl/hPTxB1', 'https://goo.gl/ZYfb2r', 'https://goo.gl/wVtdyt', 'https://goo.gl/2RtC4b',
	  'https://goo.gl/m8hDz6', 'https://goo.gl/gqfCNh', 'https://goo.gl/cNvyEg', 'https://goo.gl/BB4qrG', 'https://goo.gl/jqDHTM', 'https://goo.gl/t9UsSX',
	  'https://goo.gl/4Ecx8j', 'https://goo.gl/eAusF6', 'https://goo.gl/5YkGHs', 'https://goo.gl/61PTwY', 'https://goo.gl/DJqVHt', 'https://goo.gl/Efiuzf'];

	image_array[15] = ['https://goo.gl/tX4pvA', 'https://goo.gl/QdGFv1', 'https://goo.gl/EXzuuy', 'https://goo.gl/sp4zpL', 'https://goo.gl/gBGwKN',
	  'https://goo.gl/AuKXcq', 'https://goo.gl/boVvig', 'https://goo.gl/sJJZHg', 'https://goo.gl/h4oHjD', 'https://goo.gl/cWY2Ro', 'https://goo.gl/zQzBVj',
	  'https://goo.gl/VuuNL6'];

	image_array[16] = ['https://goo.gl/4UXECa', 'https://goo.gl/AWKAgh', 'https://goo.gl/j2kvRE', 'https://goo.gl/8wP9TS', 'https://goo.gl/gTqe4e',
	'https://goo.gl/a3Raq6', 'https://goo.gl/i8aMeK', 'https://goo.gl/WRwG1T','https://goo.gl/kmgLBz', 'https://goo.gl/rxsPEy', 'https://goo.gl/vQ4EPp',
	'https://goo.gl/QqtnRu', 'https://www.youtube.com/embed/w1XULu9uCwM', 'https://www.youtube.com/embed/ifQF8XGVY6A', 'https://goo.gl/eYdYk6',
	'https://goo.gl/b1Vayo', 'https://goo.gl/iAepPx', 'https://goo.gl/C4RrAJ', 'https://goo.gl/Gfb2nB', 'https://goo.gl/4wrwjh', 'https://goo.gl/4brv9r'];

	image_array[17] = ['https://goo.gl/NhA5q9', 'https://goo.gl/kmgiin', 'https://goo.gl/qCz8gD', 'https://goo.gl/bvdp4U', 'https://goo.gl/Q8YDD8', 'https://goo.gl/GbkF2U',
        'https://goo.gl/udwb2U', 'https://goo.gl/MWMfHd', 'https://goo.gl/PhiBjY', 'https://goo.gl/9GrY8F', 'https://goo.gl/WGmUnc'];
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { connect } from 'react-redux';
import { Trans } from '@lingui/macro';

import Icon from 'common/Icon';
import DiscordButton from 'interface/common/thirdpartybuttons/Discord';
import GitHubButton from 'interface/common/thirdpartybuttons/GitHub';
import makeAnalyzerUrl from 'interface/common/makeAnalyzerUrl';
import { ignorePreviousPatchWarning } from 'interface/actions/previousPatch';
import { getReportCodesIgnoredPreviousPatchWarning } from 'interface/selectors/skipPreviousPatchWarning';

import Background from './images/weirdnelf.png';

const EXPANSION_START = 1534197600000;
const LATEST_PATCH = 1537221600000;

class PatchChecker extends React.PureComponent {
  static propTypes = {
    children: PropTypes.node.isRequired,
    report: PropTypes.object.isRequired,
    ignorePreviousPatchWarning: PropTypes.func.isRequired,
    ignored: PropTypes.array.isRequired,
  };

  constructor() {
    super();
    this.handleClickContinue = this.handleClickContinue.bind(this);
  }

  componentDidMount() {
    ReactTooltip.rebuild();
  }
  componentDidUpdate() {
    ReactTooltip.hide();
  }

  handleClickContinue() {
    // I chose on purpose not to store this in a cookie since I don't want this to be forgotten. It should not be a big deal if this happens every time the page is loaded, so long as it isn't shown every fight.
    this.props.ignorePreviousPatchWarning(this.props.report.code);
  }

  get continue() {
    return this.props.ignored.includes(this.props.report.code);
  }

  render() {
    const { children, report } = this.props;

    const reportTimestamp = report.start;
    const reportDate = new Date(report.start).toString();

    if (reportTimestamp >= LATEST_PATCH || this.continue) {
      return children;
    } else if (reportTimestamp >= EXPANSION_START) {
      return (

{`${report.title} - ${reportDate}`}

<Trans>This report is for an earlier patch</Trans>

                  <Trans>
                    WoWAnalyzer is constantly being updated to support the latest changes. This can cause some functionality to be modified for the latest talents/traits/trinkets or be removed.

                    This could mean that some parts of your report will no longer be analysed accurately.

                    If you would still like to view the analysis, you can click 'Continue anyway' below.
                  </Trans>

                    <GitHubButton />{' '}
                    <DiscordButton />

                  <Link
                    to={makeAnalyzerUrl(report)}
                    onClick={this.handleClickContinue}
                    style={{ fontSize: '1.1em' }}
                    data-tip="Khadgar approves your bravery"
                  >
                    <Icon icon="quest_khadgar" /> <Trans>Continue anyway</Trans>
                  </Link>

      );
    } else {
      return (

{`${report.title} - ${reportDate}`}

<Trans>Sorry, this report is for a previous expansion</Trans>

                  <Trans>
                    Due to the number of class changes since the last expansion (class abilities, talents, etc.), the analysis provided by WoWAnalyzer will most likely be inaccurate.

                    You can still access the Analysis by clicking 'Continue anyway' below if required.
                  </Trans>

                    <GitHubButton />{' '}
                    <DiscordButton />

                  <Link
                    to={makeAnalyzerUrl(report)}
                    onClick={this.handleClickContinue}
                    style={{ fontSize: '1.1em' }}
                    data-tip="Khadgar approves your bravery"
                  >
                    <Icon icon="quest_khadgar" /> <Trans>Continue anyway</Trans>
                  </Link>

      );
    }
  }
}

const mapStateToProps = state => ({
  ignored: getReportCodesIgnoredPreviousPatchWarning(state),
});
export default connect(mapStateToProps, {
  ignorePreviousPatchWarning,
})(PatchChecker);
<commit_msg>Removed duplicate code<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { connect } from 'react-redux';
import { Trans } from '@lingui/macro';

import Icon from 'common/Icon';
import DiscordButton from 'interface/common/thirdpartybuttons/Discord';
import GitHubButton from 'interface/common/thirdpartybuttons/GitHub';
import makeAnalyzerUrl from 'interface/common/makeAnalyzerUrl';
import { ignorePreviousPatchWarning } from 'interface/actions/previousPatch';
import { getReportCodesIgnoredPreviousPatchWarning } from 'interface/selectors/skipPreviousPatchWarning';

import Background from './images/weirdnelf.png';

const EXPANSION_START = 1534197600000;
const LATEST_PATCH = 1537221600000;

class PatchChecker extends React.PureComponent {
  static propTypes = {
    children: PropTypes.node.isRequired,
    report: PropTypes.object.isRequired,
    ignorePreviousPatchWarning: PropTypes.func.isRequired,
    ignored: PropTypes.array.isRequired,
  };

  constructor() {
    super();
    this.handleClickContinue = this.handleClickContinue.bind(this);
  }

  componentDidMount() {
    ReactTooltip.rebuild();
  }
  componentDidUpdate() {
    ReactTooltip.hide();
  }

  handleClickContinue() {
    // I chose on purpose not to store this in a cookie since I don't want this to be forgotten. It should not be a big deal if this happens every time the page is loaded, so long as it isn't shown every fight.
    this.props.ignorePreviousPatchWarning(this.props.report.code);
  }

  get continue() {
    return this.props.ignored.includes(this.props.report.code);
  }

  render() {
    const { children, report } = this.props;

    const reportTimestamp = report.start;
    const reportDate = new Date(report.start).toString();

    if (reportTimestamp >= LATEST_PATCH || this.continue) {
      return children;
    } else {
      return (

{`${report.title} - ${reportDate}`}

<Trans>{reportTimestamp >= EXPANSION_START ? "This report is for an earlier patch" : "Sorry, this report is for a previous expansion" }</Trans>

                  <>
                  {
                    reportTimestamp >= EXPANSION_START ? (
                      <Trans>
                        WoWAnalyzer is constantly being updated to support the latest changes. This can cause some functionality to be modified for the latest talents/traits/trinkets or be removed.

                        This could mean that some parts of your report will no longer be analysed accurately.

                        If you would still like to view the analysis, you can click 'Continue anyway' below.
                      </Trans>
                    ) : (
                      <Trans>
                        Due to the number of class changes since the last expansion (class abilities, talents, etc.), the analysis provided by WoWAnalyzer will most likely be inaccurate.

                        You can still access the Analysis by clicking 'Continue anyway' below if required.
                      </Trans>
                    )
                  }
                  </>

                    <GitHubButton />{' '}
                    <DiscordButton />

                  <Link
                    to={makeAnalyzerUrl(report)}
                    onClick={this.handleClickContinue}
                    style={{ fontSize: '1.1em' }}
                    data-tip="Khadgar approves your bravery"
                  >
                    <Icon icon="quest_khadgar" /> <Trans>Continue anyway</Trans>
                  </Link>

      );
    }
  }
}

const mapStateToProps = state => ({
  ignored: getReportCodesIgnoredPreviousPatchWarning(state),
});
export default connect(mapStateToProps, {
  ignorePreviousPatchWarning,
})(PatchChecker);
<|endoftext|>


<commit_before>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.SiteGallery = Ensembl.Panel.Content.extend({

  init: function () {

    this.base.apply(this, arguments);

    var $thumbnail;

    this.el.find(".embiggen").each(
      function() {
        $thumbnail = $(this);
        $thumbnail.on({
                        mouseover: function () {
                          console.log('Thumbnail: ' + $thumbnail.attr('src'));
                          $(this).addClass('zoom');
                        }
                      });
        $thumbnail.on({
                        mouseout: function () {
                          console.log('Leaving ' + $thumbnail.attr('src'));
                          $(this).removeClass('zoom');
                        }
                      });
      }
    );
  }
});

Ensembl.Panel.SiteGalleryHome = Ensembl.Panel.Content.extend({

  init: function () {

    this.base.apply(this, arguments);

    this.elLk.form        = this.el.find('form[name=gallery_home]').remove('.js_param');
    this.elLk.dataType    = this.elLk.form.find('input[name=data_type]');
    this.elLk.identifier  = this.elLk.form.find('input[name=identifier]');
    this.elLk.species     = this.elLk.form.find('select[name=species]');

    this.formAction       = this.elLk.form.attr('action');

    this.initSelectToToggle();
    this.updateIdentifier();

    this.elLk.dataType.add(this.elLk.species).on('change', {panel: this}, function(e) { e.data.panel.updateIdentifier() });
  },

  initSelectToToggle: function () {
    var panel = this;

    this.elLk.species.find('option').addClass(function () {
      return panel.params['sample_data'][this.value]['variation'] ? '_stt__var' : '_stt__novar';
    });

    this.elLk.dataType.parent().addClass(function () {
      return $(this).find('[value=variation]').length ? '_stt_var' : '_stt_var _stt_novar';
    });

    this.elLk.species.selectToToggle();
  },

  updateIdentifier: function () {
    var species = this.elLk.species.val();

    if (!this.elLk.dataType.filter(':visible:checked').length) {
      this.elLk.dataType.filter(':visible').first().prop('checked', true);
    }

    this.elLk.identifier.val((this.params['sample_data'][species] || {})[this.elLk.dataType.filter(':checked').val()] || '');
    this.elLk.form.attr('action', this.formAction.replace('Multi',  species));
  }
});
<commit_msg>Remove console logging<commit_after>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.SiteGallery = Ensembl.Panel.Content.extend({

  init: function () {

    this.base.apply(this, arguments);

    var $thumbnail;

    this.el.find(".embiggen").each(
      function() {
        $thumbnail = $(this);
        $thumbnail.on({
                        mouseover: function () {
                          $(this).addClass('zoom');
                        }
                      });
        $thumbnail.on({
                        mouseout: function () {
                          $(this).removeClass('zoom');
                        }
                      });
      }
    );
  }
});

Ensembl.Panel.SiteGalleryHome = Ensembl.Panel.Content.extend({

  init: function () {

    this.base.apply(this, arguments);

    this.elLk.form        = this.el.find('form[name=gallery_home]').remove('.js_param');
    this.elLk.dataType    = this.elLk.form.find('input[name=data_type]');
    this.elLk.identifier  = this.elLk.form.find('input[name=identifier]');
    this.elLk.species     = this.elLk.form.find('select[name=species]');

    this.formAction       = this.elLk.form.attr('action');

    this.initSelectToToggle();
    this.updateIdentifier();

    this.elLk.dataType.add(this.elLk.species).on('change', {panel: this}, function(e) { e.data.panel.updateIdentifier() });
  },

  initSelectToToggle: function () {
    var panel = this;

    this.elLk.species.find('option').addClass(function () {
      return panel.params['sample_data'][this.value]['variation'] ? '_stt__var' : '_stt__novar';
    });

    this.elLk.dataType.parent().addClass(function () {
      return $(this).find('[value=variation]').length ? '_stt_var' : '_stt_var _stt_novar';
    });

    this.elLk.species.selectToToggle();
  },

  updateIdentifier: function () {
    var species = this.elLk.species.val();

    if (!this.elLk.dataType.filter(':visible:checked').length) {
      this.elLk.dataType.filter(':visible').first().prop('checked', true);
    }

    this.elLk.identifier.val((this.params['sample_data'][species] || {})[this.elLk.dataType.filter(':checked').val()] || '');
    this.elLk.form.attr('action', this.formAction.replace('Multi',  species));
  }
});
<|endoftext|>