diff --git a/api/stripe.php b/api/stripe.php
new file mode 100644
index 0000000..3099070
--- /dev/null
+++ b/api/stripe.php
@@ -0,0 +1,50 @@
+ [],
+ 'public' => []
+ ];
+
+ protected static $log_mysql = 'all';
+
+ protected static $cache = false;
+ protected static $cache_for = null;
+
+ /**
+ * Send an API call off to Stripe
+ *
+ * @param string $method HTTP Method.
+ * @param string $endpoint API Endpoint.
+ * @param array $data API request data.
+ *
+ * @throws Exception If Stripe did not return valid JSON.
+ *
+ * @return array The Stripe response.
+ */
+ public function stripe_api($method, $endpoint, $data) {
+ $curl_response = $this->curl([
+ 'url' => $this->setting->get('stripe_base_url') . $endpoint,
+ 'post_fields' => http_build_query($data),
+ 'method' => $method,
+ 'header' => [
+ 'Authorization: Basic ' . base64_encode($this->setting->get('stripe_secret_key') . ':'),
+ 'Content-Type: application/x-www-form-urlencoded'
+ ]
+ ]);
+
+ $response = json_decode($curl_response, true);
+
+ if ($response === null) {
+ throw new cora\exception('Invalid JSON', 10900);
+ }
+
+ return $response;
+ }
+}
diff --git a/api/stripe_api_cache.php b/api/stripe_api_cache.php
new file mode 100644
index 0000000..2bfb77a
--- /dev/null
+++ b/api/stripe_api_cache.php
@@ -0,0 +1,8 @@
+ [
+ 'read_id'
+ ],
+ 'public' => []
+ ];
+
+}
diff --git a/api/stripe_payment_link.php b/api/stripe_payment_link.php
new file mode 100644
index 0000000..eba58a4
--- /dev/null
+++ b/api/stripe_payment_link.php
@@ -0,0 +1,94 @@
+ [
+ 'get',
+ 'open'
+ ],
+ 'public' => []
+ ];
+
+ public static $user_locked = false;
+
+ /**
+ * Get a stripe_payment_link for the specified attributes. If none exists,
+ * create.
+ *
+ * @param array $attributes
+ *
+ * @return array
+ */
+ public function get($attributes) {
+ $stripe_payment_link = parent::get([
+ 'amount' => $attributes['amount'],
+ 'currency' => $attributes['currency'],
+ 'interval' => $attributes['interval']
+ ]);
+
+ if($stripe_payment_link === null) {
+ $price = $this->api(
+ 'stripe',
+ 'stripe_api',
+ [
+ 'method' => 'POST',
+ 'endpoint' => 'prices',
+ 'data' => [
+ 'product' => $this->setting->get('stripe_product_id'),
+ 'unit_amount' => $attributes['amount'],
+ 'currency' => $attributes['currency'],
+ 'recurring[interval]' => $attributes['interval']
+ ]
+ ]
+ );
+
+ $payment_link = $this->api(
+ 'stripe',
+ 'stripe_api',
+ [
+ 'method' => 'POST',
+ 'endpoint' => 'payment_links',
+ 'data' => [
+ 'line_items[0][price]' => $price['id'],
+ 'line_items[0][quantity]' => '1'
+ ]
+ ]
+ );
+
+ return $this->create([
+ 'amount' => $attributes['amount'],
+ 'currency' => $attributes['currency'],
+ 'interval' => $attributes['interval'],
+ 'url' => $payment_link['url']
+ ]);
+ } else {
+ return $stripe_payment_link;
+ }
+ }
+
+ /**
+ * Open a Stripe link. This exists because in JS it would be a popup to run
+ * an API call to get the link, then do window.open after. This lets you
+ * just do a window.open directly to this endpoint.
+ *
+ * @param array $attributes
+ */
+ public function open($attributes) {
+ $stripe_payment_link = $this->get($attributes);
+
+ $user = $this->api('user', 'get', $this->session->get_user_id());
+ $url = $stripe_payment_link['url'] .
+ '?prefilled_email=' . $user['email_address'] .
+ '&client_reference_id=' . $user['user_id'];
+
+ header('Location: ' . $url);
+ die();
+ }
+
+}
diff --git a/css/dashboard.css b/css/dashboard.css
index 83e27eb..781678b 100644
--- a/css/dashboard.css
+++ b/css/dashboard.css
@@ -454,6 +454,7 @@ input[type=range]::-moz-range-thumb {
.icon.code_tags:before { content: "\F0174"; }
.icon.cog:before { content: "\F0493"; }
.icon.credit_card_lock:before { content: "\F18E7"; }
+.icon.credit_card_settings:before { content: "\F0FF5"; }
.icon.currency_usd:before { content: "\F01C1"; }
.icon.delete:before { content: "\F01B4"; }
.icon.dots_vertical:before { content: "\F01D9"; }
@@ -468,6 +469,7 @@ input[type=range]::-moz-range-thumb {
.icon.fast_forward:before { content: "\F0211"; }
.icon.fire:before { content: "\F0238"; }
.icon.floor_plan:before { content: "\F0821"; }
+.icon.forum:before { content: "\F028C"; }
.icon.gift:before { content: "\F0E44"; }
.icon.google_play:before { content: "\F02BC"; }
.icon.grid:before { content: "\F02C1"; }
@@ -481,6 +483,8 @@ input[type=range]::-moz-range-thumb {
.icon.information:before { content: "\F02FC"; }
.icon.key:before { content: "\F0306"; }
.icon.label:before { content: "\F0315"; }
+.icon.label:before { content: "\F0315"; }
+.icon.label_off:before { content: "\F0ACB"; }
.icon.layers:before { content: "\F0328"; }
.icon.layers_plus:before { content: "\F0E4D"; }
.icon.link_off:before { content: "\F0338"; }
@@ -516,6 +520,7 @@ input[type=range]::-moz-range-thumb {
.icon.numeric_8_box:before { content: "\F03B9"; }
.icon.numeric_9:before { content: "\F0B42"; }
.icon.numeric_9_box:before { content: "\F03BC"; }
+.icon.octagram:before { content: "\F06F9"; }
.icon.open_in_new:before { content: "\F03CC"; }
.icon.patreon:before { content: "\F0882"; }
.icon.pause:before { content: "\F03E4"; }
@@ -529,6 +534,7 @@ input[type=range]::-moz-range-thumb {
.icon.restart:before { content: "\F0709"; }
.icon.restart_off:before { content: "\F0D95"; }
.icon.snowflake:before { content: "\F0717"; }
+.icon.sticker_emoji:before { content: "\F0785"; }
.icon.swap_horizontal:before { content: "\F04E1"; }
.icon.thermometer:before { content: "\F050F"; }
.icon.thermostat:before { content: "\F0393"; }
@@ -558,8 +564,6 @@ input[type=range]::-moz-range-thumb {
.icon.wifi_strength_1_alert:before { content: "\F0920"; }
.icon.wifi_strength_4:before { content: "\F0928"; }
.icon.zigbee:before { content: "\F0D41"; }
-.icon.label:before { content: "\F0315"; }
-.icon.label_off:before { content: "\F0ACB"; }
.icon.f16:before { font-size: 16px; }
.icon.f24:before { font-size: 24px; }
diff --git a/img/merchandise/sticker_logo.png b/img/merchandise/sticker_logo.png
new file mode 100644
index 0000000..d35c08f
Binary files /dev/null and b/img/merchandise/sticker_logo.png differ
diff --git a/img/merchandise/sticker_logo_text.png b/img/merchandise/sticker_logo_text.png
new file mode 100644
index 0000000..16dd989
Binary files /dev/null and b/img/merchandise/sticker_logo_text.png differ
diff --git a/js/beestat/setting.js b/js/beestat/setting.js
index cdc10c6..8cef0e7 100644
--- a/js/beestat/setting.js
+++ b/js/beestat/setting.js
@@ -83,7 +83,9 @@ beestat.setting = function(argument_1, opt_value, opt_callback) {
'visualize.three_d.show_labels': false,
'visualize.three_d.auto_rotate': false,
- 'date_format': 'M/D/YYYY'
+ 'date_format': 'M/D/YYYY',
+
+ 'units.currency': 'usd'
};
// Figure out what we're trying to do.
diff --git a/js/beestat/user.js b/js/beestat/user.js
index 877ea73..7d036c9 100644
--- a/js/beestat/user.js
+++ b/js/beestat/user.js
@@ -6,13 +6,32 @@ beestat.user = {};
* @return {boolean} true if yes, false if no.
*/
beestat.user.patreon_is_active = function() {
- var user = beestat.user.get();
+ const user = beestat.user.get();
return (
user.patreon_status !== null &&
user.patreon_status.patron_status === 'active_patron'
);
};
+/**
+ * Determine whether or not the current user is an active Stripe giver.
+ *
+ * @return {boolean} true if yes, false if no.
+ */
+beestat.user.stripe_is_active = function() {
+ const stripe_events = Object.values(beestat.cache.stripe_event);
+ for (let i = 0; i < stripe_events.length; i++) {
+ if (
+ stripe_events[i].type === 'invoice.paid' &&
+ moment.unix(stripe_events[i].data.period_end).isAfter(moment()) === false
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
/**
* Is the user connected to Patreon.
*
@@ -25,15 +44,13 @@ beestat.user.patreon_is_connected = function() {
/**
* Whether or not the current user gets access to early release features.
*
- * @return {boolean} Early access or not.
+ * @return {boolean}
*/
beestat.user.has_early_access = function() {
- var user = beestat.user.get();
+ const user = beestat.user.get();
return user.user_id === 1 ||
- (
- user.patreon_status !== null &&
- user.patreon_status.patron_status === 'active_patron'
- );
+ beestat.user.stripe_is_active() === true ||
+ beestat.user.patreon_is_active() === true;
};
/**
@@ -42,6 +59,6 @@ beestat.user.has_early_access = function() {
* @return {object} The current user.
*/
beestat.user.get = function() {
- var user_id = Object.keys(beestat.cache.user)[0];
+ const user_id = Object.keys(beestat.cache.user)[0];
return beestat.cache.user[user_id];
};
diff --git a/js/component/card/contribute.js b/js/component/card/contribute.js
index 2b61db0..ae245b0 100644
--- a/js/component/card/contribute.js
+++ b/js/component/card/contribute.js
@@ -4,27 +4,15 @@
beestat.component.card.contribute = function() {
beestat.component.card.apply(this, arguments);
+ this.state_.currency_multiplier = beestat.setting('units.currency') === 'usd' ? 1 : 1.5;
this.state_.contribute_type = 'recurring';
- this.state_.contribute_frequency = 'yearly';
- this.state_.contribute_amount = 2;
- this.state_.contribute_amount_other = 5;
+ this.state_.contribute_interval = 'year';
+ this.state_.contribute_amount = 2 * this.state_.currency_multiplier;
+ this.state_.contribute_amount_other = 5 * this.state_.currency_multiplier;
this.state_.contribute_amount_other_enabled = false;
};
beestat.extend(beestat.component.card.contribute, beestat.component.card);
-beestat.component.card.contribute.payment_links = {
- 'monthly': {
- '1': 'https://buy.stripe.com/test_6oEeYk3dG2mmbhC28f',
- '2': 'https://buy.stripe.com/test_dR6eYk29C6CC99ubIR',
- '3': 'https://buy.stripe.com/test_eVadUg29C5yyadybIQ'
- },
- 'yearly': {
- '12': 'https://buy.stripe.com/test_9AQ2by29CaSSgBW3cg',
- '24': 'https://buy.stripe.com/test_eVa4jGdSk2mmgBW5kq',
- '36': 'https://buy.stripe.com/test_4gwg2o7tWe5485q6ot'
- }
-};
-
/**
* Decorate.
*
@@ -33,190 +21,258 @@ beestat.component.card.contribute.payment_links = {
beestat.component.card.contribute.prototype.decorate_contents_ = function(parent) {
const self = this;
- const p1 = document.createElement('p');
- p1.innerText = 'Beestat is completely free to use and does not run ads or sell your data. If you find this service useful, please consider contributing. Your gift directly supports operating costs and project development.';
- parent.appendChild(p1);
+ if (beestat.user.has_early_access() === true) {
+ const p1 = document.createElement('p');
+ p1.innerText = 'Thank you so much for your support! Your contribution goes a long way towards keeping beestat running and enabling me to create a rich and useful application. My hope is that you are able to use beestat to save money and improve the efficiency of your home.';
+ parent.appendChild(p1);
- const p2 = document.createElement('p');
- p2.innerText = 'I would like to give...';
- parent.appendChild(p2);
+ const tile_group = new beestat.component.tile_group();
- // Amount
- const amount_container = document.createElement('div');
- Object.assign(amount_container.style, {
- 'display': 'flex',
- 'flex-wrap': 'wrap',
- 'align-items': 'center',
- 'grid-gap': `${beestat.style.size.gutter}px`
- });
- parent.appendChild(amount_container);
-
- const tile_group_amount = new beestat.component.tile_group();
- const amounts = [
- 1,
- 2,
- 3
- ];
- amounts.forEach(function(amount) {
- const tile_amount = new beestat.component.tile()
- .set_background_color(beestat.style.color.bluegray.light)
- .set_background_hover_color(beestat.style.color.green.base)
+ const discord_tile = new beestat.component.tile()
+ .set_background_color(beestat.style.color.purple.base)
+ .set_background_hover_color(beestat.style.color.purple.light)
.set_text_color('#fff')
- .set_text('$' + amount + ' / month');
- tile_group_amount.add_tile(tile_amount);
+ .set_icon('forum')
+ .set_text('Join the Private Discord');
+ discord_tile.addEventListener('click', function() {
+ window.open('https://discord.gg/GzWbhD6tSB');
+ });
+ tile_group.add_tile(discord_tile);
- if (
- amount === self.state_.contribute_amount &&
- self.state_.contribute_amount_other_enabled === false
- ) {
- tile_amount
- .set_background_color(beestat.style.color.green.base);
- } else {
- tile_amount
- .set_background_color(beestat.style.color.bluegray.light)
- .set_background_hover_color(beestat.style.color.green.base);
+ const community_tile = new beestat.component.tile()
+ .set_background_color(beestat.style.color.purple.base)
+ .set_background_hover_color(beestat.style.color.purple.light)
+ .set_text_color('#fff')
+ .set_icon('forum')
+ .set_text('Join the beestat Community');
+ community_tile.addEventListener('click', function() {
+ window.open('https://community.beestat.io');
+ });
+ tile_group.add_tile(community_tile);
- tile_amount.addEventListener('click', function() {
- self.state_.contribute_amount_other_enabled = false;
- self.state_.contribute_amount = amount;
- self.rerender();
- });
- }
- });
+ tile_group.render(parent);
- const tile_amount_other = new beestat.component.tile()
- .set_text_color('#fff')
- .set_text('Other Amount');
- if (this.state_.contribute_amount_other_enabled === true) {
- tile_amount_other
- .set_background_color(beestat.style.color.green.base);
+ const p2 = document.createElement('p');
+ p2.innerText = 'Looking for a way to contribute more but don\'t want to change your recurring support amount?';
+ parent.appendChild(p2);
+
+ const one_time_gift_tile = new beestat.component.tile()
+ .set_background_color(beestat.style.color.green.base)
+ .set_background_hover_color(beestat.style.color.green.light)
+ .set_text_color('#fff')
+ .set_icon('gift')
+ .set_text('Make a One-Time Gift');
+ one_time_gift_tile.addEventListener('click', function() {
+ window.open('https://donate.stripe.com/7sIcPp4Gt6APais144');
+ });
+
+ one_time_gift_tile.render(parent);
} else {
- tile_amount_other
- .set_background_color(beestat.style.color.bluegray.light)
- .set_background_hover_color(beestat.style.color.green.base);
- }
+ const p3 = document.createElement('p');
+ p3.innerText = 'Beestat is completely free to use and does not run ads or sell your data. If you find this service useful, please consider contributing. Your gift directly supports operating costs and project development.';
+ parent.appendChild(p3);
- tile_amount_other.addEventListener('click', function() {
- self.state_.contribute_amount_other_enabled = true;
- self.rerender();
- });
- tile_group_amount.add_tile(tile_amount_other);
+ const p4 = document.createElement('p');
+ p4.innerText = 'I would like to give...';
+ parent.appendChild(p4);
- tile_group_amount.render($(amount_container));
+ // Amount
+ const amount_container = document.createElement('div');
+ Object.assign(amount_container.style, {
+ 'display': 'flex',
+ 'flex-wrap': 'wrap',
+ 'align-items': 'center',
+ 'grid-gap': `${beestat.style.size.gutter}px`
+ });
+ parent.appendChild(amount_container);
- if (this.state_.contribute_amount_other_enabled === true) {
- const other_amount_input = new beestat.component.input.text()
- .set_width(100)
- .set_icon('currency_usd')
- .set_value(this.state_.contribute_amount_other)
- .set_maxlength(6)
- .set_requirements({
- 'required': true,
- 'type': 'number',
- 'min_value': 1,
- 'max_value': 100
- })
- .set_transform({
- 'type': 'round',
- 'decimals': 2
- });
- other_amount_input.addEventListener('blur', function() {
- if (this.meets_requirements() === true) {
- self.state_.contribute_amount_other = this.get_value();
- self.rerender();
+ const tile_group_amount = new beestat.component.tile_group();
+
+ const amounts = [
+ 1 * this.state_.currency_multiplier,
+ 2 * this.state_.currency_multiplier,
+ 3 * this.state_.currency_multiplier
+ ];
+ amounts.forEach(function(amount) {
+ const tile_amount = new beestat.component.tile()
+ .set_background_color(beestat.style.color.bluegray.light)
+ .set_background_hover_color(beestat.style.color.green.base)
+ .set_text_color('#fff')
+ .set_text('$' + amount + ' / month');
+ tile_group_amount.add_tile(tile_amount);
+
+ if (
+ amount === self.state_.contribute_amount &&
+ self.state_.contribute_amount_other_enabled === false
+ ) {
+ tile_amount
+ .set_background_color(beestat.style.color.green.base);
+ } else {
+ tile_amount
+ .set_background_color(beestat.style.color.bluegray.light)
+ .set_background_hover_color(beestat.style.color.green.base);
+
+ tile_amount.addEventListener('click', function() {
+ self.state_.contribute_amount_other_enabled = false;
+ self.state_.contribute_amount = amount;
+ self.rerender();
+ });
}
});
- other_amount_input.render($(amount_container));
- }
- // Frequency
- const tile_group_frequency = new beestat.component.tile_group();
- tile_group_frequency.style({
- 'margin-top': `${beestat.style.size.gutter}px`
- });
-
- const frequencies = [
- 'yearly',
- 'monthly'
- ];
- frequencies.forEach(function(frequency) {
- const tile_frequency = new beestat.component.tile()
- .set_background_color(beestat.style.color.bluegray.light)
- .set_background_hover_color(beestat.style.color.lightblue.base)
+ const tile_amount_other = new beestat.component.tile()
.set_text_color('#fff')
- .set_text(frequency.charAt(0).toUpperCase() + frequency.slice(1));
- tile_group_frequency.add_tile(tile_frequency);
-
- if (frequency === self.state_.contribute_frequency) {
- tile_frequency
- .set_background_color(beestat.style.color.lightblue.base);
+ .set_text('Other Amount');
+ if (this.state_.contribute_amount_other_enabled === true) {
+ tile_amount_other
+ .set_background_color(beestat.style.color.green.base);
} else {
- tile_frequency
+ tile_amount_other
.set_background_color(beestat.style.color.bluegray.light)
- .set_background_hover_color(beestat.style.color.lightblue.base);
-
- tile_frequency.addEventListener('click', function() {
- self.state_.contribute_frequency = frequency;
- self.rerender();
- });
+ .set_background_hover_color(beestat.style.color.green.base);
}
- });
- tile_group_frequency.render(parent);
+ tile_amount_other.addEventListener('click', function() {
+ self.state_.contribute_amount_other_enabled = true;
+ self.rerender();
+ });
+ tile_group_amount.add_tile(tile_amount_other);
- // Review
- const review_container = document.createElement('div');
- Object.assign(review_container.style, {
- 'padding': `${beestat.style.size.gutter}px`,
- 'background': beestat.style.color.bluegray.dark,
- 'margin-top': `${beestat.style.size.gutter}px`,
- 'margin-bottom': `${beestat.style.size.gutter}px`
- });
+ tile_group_amount.render($(amount_container));
- let contribute_amount;
- if (this.state_.contribute_amount_other_enabled === true) {
- contribute_amount = this.state_.contribute_amount_other;
- } else {
- contribute_amount = this.state_.contribute_amount;
+ if (this.state_.contribute_amount_other_enabled === true) {
+ const other_amount_input = new beestat.component.input.text()
+ .set_width(100)
+ .set_icon('currency_usd')
+ .set_value(Number(this.state_.contribute_amount_other).toFixed(2))
+ .set_maxlength(6)
+ .set_requirements({
+ 'required': true,
+ 'type': 'decimal',
+ 'min_value': 1
+ })
+ .set_transform({
+ 'type': 'round',
+ 'decimals': 2
+ });
+ other_amount_input.addEventListener('blur', function() {
+ if (this.meets_requirements() === true) {
+ self.state_.contribute_amount_other = this.get_value();
+ self.rerender();
+ }
+ });
+ other_amount_input.render($(amount_container));
+ }
+
+ // Frequency
+ const tile_group_frequency = new beestat.component.tile_group();
+ tile_group_frequency.style({
+ 'margin-top': `${beestat.style.size.gutter}px`
+ });
+
+ const intervals = [
+ 'year',
+ 'month'
+ ];
+ intervals.forEach(function(frequency) {
+ const tile_frequency = new beestat.component.tile()
+ .set_background_color(beestat.style.color.bluegray.light)
+ .set_background_hover_color(beestat.style.color.lightblue.base)
+ .set_text_color('#fff')
+ .set_text('Pay ' + frequency.charAt(0).toUpperCase() + frequency.slice(1) + 'ly');
+ tile_group_frequency.add_tile(tile_frequency);
+
+ if (frequency === self.state_.contribute_interval) {
+ tile_frequency
+ .set_background_color(beestat.style.color.lightblue.base);
+ } else {
+ tile_frequency
+ .set_background_color(beestat.style.color.bluegray.light)
+ .set_background_hover_color(beestat.style.color.lightblue.base);
+
+ tile_frequency.addEventListener('click', function() {
+ self.state_.contribute_interval = frequency;
+ self.rerender();
+ });
+ }
+ });
+
+ tile_group_frequency.render(parent);
+
+ // Review
+ const review_container = document.createElement('div');
+ Object.assign(review_container.style, {
+ 'padding': `${beestat.style.size.gutter}px`,
+ 'background': beestat.style.color.bluegray.dark,
+ 'margin-top': `${beestat.style.size.gutter}px`,
+ 'margin-bottom': `${beestat.style.size.gutter}px`
+ });
+
+ let contribute_amount;
+ if (this.state_.contribute_amount_other_enabled === true) {
+ contribute_amount = this.state_.contribute_amount_other;
+ } else {
+ contribute_amount = this.state_.contribute_amount;
+ }
+
+ if (this.state_.contribute_interval === 'year') {
+ contribute_amount *= 12;
+ }
+
+ contribute_amount = Math.round(contribute_amount * 100) / 100;
+ const contribute_amount_formatted = new Intl.NumberFormat('en-US', {
+ 'style': 'currency',
+ 'currency': beestat.setting('units.currency')
+ }).format(contribute_amount);
+ const contribute_interval = this.state_.contribute_interval;
+
+ review_container.innerText = 'Give ' + contribute_amount_formatted + ' / ' + contribute_interval + ' until I cancel.';
+ parent.appendChild(review_container);
+
+ // Buttons
+ const button_container = document.createElement('div');
+ Object.assign(button_container.style, {
+ 'text-align': 'right'
+ });
+ parent.appendChild($(button_container));
+
+ const continue_tile = new beestat.component.tile()
+ .set_background_color(beestat.style.color.green.base)
+ .set_background_hover_color(beestat.style.color.green.light)
+ .set_text_color('#fff')
+ .set_size('large')
+ .set_icon('credit_card_lock')
+ .set_text('Continue to Payment');
+ continue_tile.addEventListener('click', function() {
+ self.state_.stripe_connecting = true;
+
+ this
+ .set_background_color(beestat.style.color.gray.base)
+ .set_background_hover_color()
+ .removeEventListener('click');
+
+ window.open('https://dev.app.beestat.io/api/?resource=stripe_payment_link&method=open&arguments={"attributes":{"amount":' + (contribute_amount * 100) + ',"currency":"' + beestat.setting('units.currency') + '","interval":"' + contribute_interval + '"}}&api_key=' + beestat.api.api_key);
+
+ setTimeout(function() {
+ self.rerender();
+ }, 5000);
+ });
+
+ continue_tile.render($(button_container));
+
+ if (this.state_.stripe_connecting === true) {
+ const api_call = new beestat.api()
+ .add_call('stripe_event', 'read_id')
+ .set_callback(function(response) {
+ beestat.cache.set('stripe_event', response);
+ });
+
+ window.setTimeout(function() {
+ api_call.send();
+ self.rerender();
+ }, 5000);
+ }
}
-
- if (this.state_.contribute_frequency === 'yearly') {
- contribute_amount *= 12;
- }
-
- contribute_amount = Math.round(contribute_amount * 100) / 100;
- const contribute_amount_formatted = new Intl.NumberFormat('en-US', {
- 'style': 'currency',
- 'currency': 'USD'
- }).format(contribute_amount);
- const contribute_frequency = this.state_.contribute_frequency.replace('ly', '');
-
- review_container.innerText = 'Give ' + contribute_amount_formatted + ' / ' + contribute_frequency + ' until I cancel.';
- parent.appendChild(review_container);
-
- // Buttons
- const button_container = document.createElement('div');
- Object.assign(button_container.style, {
- 'text-align': 'right'
- });
- parent.appendChild($(button_container));
-
- const continue_tile = new beestat.component.tile()
- .set_background_color(beestat.style.color.green.base)
- .set_background_hover_color(beestat.style.color.green.light)
- .set_text_color('#fff')
- .set_size('large')
- .set_icon('credit_card_lock')
- .set_text('Continue to Payment');
- continue_tile.addEventListener('click', function() {
- window.open(
- beestat.component.card.contribute.payment_links[self.state_.contribute_frequency][contribute_amount] +
- '?prefilled_email=' + beestat.user.get().email_address +
- '&client_reference_id=' + beestat.user.get().user_id
- );
- });
-
- continue_tile.render($(button_container));
};
/**
@@ -234,19 +290,21 @@ beestat.component.card.contribute.prototype.get_title_ = function() {
* @param {rocket.Elements} parent
*/
beestat.component.card.contribute.prototype.decorate_top_right_ = function(parent) {
- const menu = (new beestat.component.menu()).render(parent);
+ if (beestat.user.has_early_access() === false) {
+ const menu = (new beestat.component.menu()).render(parent);
- menu.add_menu_item(new beestat.component.menu_item()
- .set_text('One-Time Gift')
- .set_icon('gift')
- .set_callback(function() {
- window.open('https://donate.stripe.com/test_bIY2by6pS1ii99u8wG');
- }));
+ menu.add_menu_item(new beestat.component.menu_item()
+ .set_text('One-Time Gift')
+ .set_icon('gift')
+ .set_callback(function() {
+ window.open('https://donate.stripe.com/7sIcPp4Gt6APais144');
+ }));
- menu.add_menu_item(new beestat.component.menu_item()
- .set_text('Help')
- .set_icon('help_circle')
- .set_callback(function() {
- window.open('https://doc.beestat.io/TODO');
- }));
+ menu.add_menu_item(new beestat.component.menu_item()
+ .set_text('Support on Patreon')
+ .set_icon('patreon')
+ .set_callback(function() {
+ window.open('https://patreon.com/beestat');
+ }));
+ }
};
diff --git a/js/component/card/contribute_benefits.js b/js/component/card/contribute_benefits.js
new file mode 100644
index 0000000..4996d85
--- /dev/null
+++ b/js/component/card/contribute_benefits.js
@@ -0,0 +1,58 @@
+/**
+ * Contribute benefits.
+ */
+beestat.component.card.contribute_benefits = function() {
+ beestat.component.card.apply(this, arguments);
+};
+beestat.extend(beestat.component.card.contribute_benefits, beestat.component.card);
+
+/**
+ * Decorate.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.contribute_benefits.prototype.decorate_contents_ = function(parent) {
+ const p = document.createElement('p');
+ p.innerText = 'In addition to satisfaction of supporting a great project, you\'ll get:';
+ parent.appendChild(p);
+
+ const benefit_container = document.createElement('div');
+ Object.assign(benefit_container.style, {
+ 'background': beestat.style.color.bluegray.dark,
+ 'padding': `${beestat.style.size.gutter}px`
+ });
+ parent.appendChild(benefit_container);
+
+ const benefits = [
+ 'Early access to new features',
+ 'Private Discord membership',
+ 'More frequent data syncing'
+ ];
+ benefits.forEach(function(benefit) {
+ new beestat.component.tile()
+ .set_shadow(false)
+ .set_text_color(beestat.style.color.yellow.base)
+ .set_icon('octagram')
+ .set_text(benefit)
+ .style({
+ 'margin-bottom': `${beestat.style.size.gutter}px`
+ })
+ .render($(benefit_container));
+ });
+
+ new beestat.component.tile()
+ .set_shadow(false)
+ .set_text_color(beestat.style.color.red.base)
+ .set_icon('heart')
+ .set_text('My unending gratitude')
+ .render($(benefit_container));
+};
+
+/**
+ * Get the title of the card.
+ *
+ * @return {string} The title.
+ */
+beestat.component.card.contribute_benefits.prototype.get_title_ = function() {
+ return 'Benefits';
+};
diff --git a/js/component/card/contribute_status.js b/js/component/card/contribute_status.js
new file mode 100644
index 0000000..92b61d1
--- /dev/null
+++ b/js/component/card/contribute_status.js
@@ -0,0 +1,237 @@
+/**
+ * Contribute benefits.
+ */
+beestat.component.card.contribute_status = function() {
+ const self = this;
+
+ beestat.dispatcher.addEventListener(
+ [
+ 'cache.user',
+ 'cache.stripe_event'
+ ],
+ function() {
+ self.rerender();
+ }
+ );
+
+ beestat.component.card.apply(this, arguments);
+};
+beestat.extend(beestat.component.card.contribute_status, beestat.component.card);
+
+/**
+ * Decorate.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.contribute_status.prototype.decorate_contents_ = function(parent) {
+ const p = document.createElement('p');
+ p.innerText = 'Use this area to view, update, or cancel your support of beestat.';
+ parent.appendChild(p);
+
+ this.decorate_direct_(parent);
+ this.decorate_patreon_(parent);
+};
+
+/**
+ * Decorate direct giving.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.contribute_status.prototype.decorate_direct_ = function(parent) {
+ (new beestat.component.title('Direct Giving')).render(parent);
+
+ const container = document.createElement('div');
+ Object.assign(container.style, {
+ 'background': beestat.style.color.bluegray.dark,
+ 'padding': `${beestat.style.size.gutter}px`,
+ 'display': 'flex',
+ 'flex-wrap': 'wrap',
+ 'grid-gap': `${beestat.style.size.gutter}px`,
+ 'align-items': 'center',
+ 'margin-bottom': `${beestat.style.size.gutter}px`
+ });
+ parent.appendChild(container);
+
+ const status_container = document.createElement('div');
+ Object.assign(status_container.style, {
+ 'flex-grow': '1'
+ });
+ container.appendChild(status_container);
+
+ const status_tile = new beestat.component.tile()
+ .set_shadow(false);
+
+ const button_container = document.createElement('div');
+ container.appendChild(button_container);
+
+ if (beestat.user.stripe_is_active() === true) {
+ status_tile
+ .set_icon('check')
+ .set_text_color(beestat.style.color.green.base)
+ .set_text('Supporter');
+ } else {
+ status_tile
+ .set_icon('close')
+ .set_text_color(beestat.style.color.gray.base)
+ .set_text('Not a Supporter');
+ }
+ status_tile.render($(status_container));
+
+ const manage_tile = new beestat.component.tile()
+ .set_text('Manage Support')
+ .set_icon('credit_card_settings')
+ .set_background_color(beestat.style.color.red.base)
+ .set_background_hover_color(beestat.style.color.red.light)
+ .set_text_color('#fff')
+ .addEventListener('click', function() {
+ window.open(
+ window.environment === 'dev'
+ ? 'https://billing.stripe.com/p/login/test_14k8zD2vwb8g6ZO8ww'
+ : 'https://billing.stripe.com/p/login/7sI5kEetRfHP6g8fYY'
+ );
+ });
+ manage_tile.render($(button_container));
+};
+
+/**
+ * Decorate Patreon giving.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.contribute_status.prototype.decorate_patreon_ = function(parent) {
+ const self = this;
+
+ (new beestat.component.title('Patreon')).render(parent);
+
+ const container = document.createElement('div');
+ Object.assign(container.style, {
+ 'background': beestat.style.color.bluegray.dark,
+ 'padding': `${beestat.style.size.gutter}px`,
+ 'display': 'flex',
+ 'flex-wrap': 'wrap',
+ 'grid-gap': `${beestat.style.size.gutter}px`,
+ 'align-items': 'center'
+ });
+ parent.appendChild(container);
+
+ const status_container = document.createElement('div');
+ Object.assign(status_container.style, {
+ 'flex-grow': '1'
+ });
+ container.appendChild(status_container);
+
+ const status_tile = new beestat.component.tile()
+ .set_shadow(false);
+
+ const button_container = document.createElement('div');
+ container.appendChild(button_container);
+
+ if (beestat.user.patreon_is_connected() === true) {
+ self.state_.patreon_connecting = false;
+
+ const user = beestat.user.get();
+
+ if (user.patreon_status.patron_status === 'active_patron') {
+ status_tile
+ .set_icon('check')
+ .set_text_color(beestat.style.color.green.base)
+ .set_text('Supporter');
+ } else {
+ status_tile
+ .set_icon('close')
+ .set_text_color(beestat.style.color.gray.base)
+ .set_text('Not a Supporter');
+ }
+ status_tile.render($(status_container));
+
+ const tile_group = new beestat.component.tile_group();
+ const unlink_tile = new beestat.component.tile()
+ .set_text('Unlink')
+ .set_icon('link_off')
+ .set_shadow(false)
+ .set_text_color(beestat.style.color.gray.base)
+ .set_text_hover_color(beestat.style.color.red.base)
+ .addEventListener('click', function() {
+ this.removeEventListener('click');
+
+ new beestat.api()
+ .add_call(
+ 'user',
+ 'unlink_patreon_account',
+ {},
+ 'unlink_patreon_account'
+ )
+ .add_call('user', 'read_id', {}, 'user')
+ .set_callback(function(response) {
+ beestat.cache.set('user', response.user);
+ })
+ .send();
+ });
+ tile_group.add_tile(unlink_tile);
+
+ const manage_tile = new beestat.component.tile()
+ .set_text('Manage Support')
+ .set_icon('patreon')
+ .set_background_color(beestat.style.color.red.base)
+ .set_background_hover_color(beestat.style.color.red.light)
+ .set_text_color('#fff')
+ .addEventListener('click', function() {
+ window.open('https://patreon.com/beestat');
+ });
+ tile_group.add_tile(manage_tile);
+
+ tile_group.render($(button_container));
+ } else {
+ status_tile
+ .set_icon('cloud_question')
+ .set_text_color(beestat.style.color.gray.base)
+ .set_text('Account Not Connected')
+ .render($(status_container));
+
+ if (this.state_.patreon_connecting === true) {
+ const connecting_button = new beestat.component.tile()
+ .set_text('Cancel Connection')
+ .set_icon('close')
+ .set_background_color(beestat.style.color.red.base)
+ .set_background_hover_color(beestat.style.color.red.light)
+ .set_text_color('#fff')
+ .addEventListener('click', function() {
+ self.state_.patreon_connecting = false;
+ self.rerender();
+ });
+ connecting_button.render($(button_container));
+
+ const api_call = new beestat.api()
+ .add_call('user', 'read_id')
+ .set_callback(function(response) {
+ beestat.cache.set('user', response);
+ });
+
+ window.setTimeout(function() {
+ api_call.send();
+ }, 5000);
+ } else {
+ const link_button = new beestat.component.tile()
+ .set_text('Connect Account')
+ .set_icon('patreon')
+ .set_background_color(beestat.style.color.red.base)
+ .set_background_hover_color(beestat.style.color.red.light)
+ .set_text_color('#fff')
+ .addEventListener('click', function() {
+ window.open('../api/?resource=patreon&method=authorize&arguments={}&api_key=ER9Dz8t05qUdui0cvfWi5GiVVyHP6OB8KPuSisP2');
+ self.state_.patreon_connecting = true;
+ self.rerender();
+ });
+ link_button.render($(button_container));
+ }
+ }
+};
+
+/**
+ * Get the title of the card.
+ *
+ * @return {string} The title.
+ */
+beestat.component.card.contribute_status.prototype.get_title_ = function() {
+ return 'Contribution Status';
+};
diff --git a/js/component/card/merchandise.js b/js/component/card/merchandise.js
new file mode 100644
index 0000000..7a2bf4b
--- /dev/null
+++ b/js/component/card/merchandise.js
@@ -0,0 +1,130 @@
+/**
+ * Contribute benefits.
+ */
+beestat.component.card.merchandise = function() {
+ beestat.component.card.apply(this, arguments);
+};
+beestat.extend(beestat.component.card.merchandise, beestat.component.card);
+
+/**
+ * Decorate.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.merchandise.prototype.decorate_contents_ = function(parent) {
+ const p = document.createElement('p');
+ p.innerText = 'Slap a sticker on your furnace and another on a water bottle to support your favorite thermostat analytics platform! Stickers are high quality, made in the USA, and shipped within 1 business day.';
+ parent.appendChild(p);
+
+ const flex_container = document.createElement('div');
+ Object.assign(flex_container.style, {
+ 'display': 'flex',
+ 'grid-gap': `${beestat.style.size.gutter}px`,
+ 'align-items': 'center',
+ 'flex-wrap': 'wrap'
+ });
+ parent.appendChild(flex_container);
+
+ const sticker_logo_text_container = document.createElement('div');
+ Object.assign(sticker_logo_text_container.style, {
+ 'background': beestat.style.color.gray.dark,
+ 'padding': `${beestat.style.size.gutter}px`,
+ 'border-radius': `${beestat.style.size.border_radius}px`,
+ 'text-align': 'right',
+ 'color': beestat.style.color.gray.dark
+ });
+ const sticker_logo_text_img = document.createElement('img');
+ sticker_logo_text_img.setAttribute('src', 'img/merchandise/sticker_logo_text.png');
+ sticker_logo_text_img.style.height = '48px';
+ sticker_logo_text_img.style.marginTop = '8px';
+ sticker_logo_text_img.style.marginBottom = '8px';
+ sticker_logo_text_container.appendChild(sticker_logo_text_img);
+ flex_container.appendChild(sticker_logo_text_container);
+
+ const sticker_logo_container = document.createElement('div');
+ Object.assign(sticker_logo_container.style, {
+ 'background': beestat.style.color.gray.dark,
+ 'padding': `${beestat.style.size.gutter}px`,
+ 'border-radius': `${beestat.style.size.border_radius}px`,
+ 'text-align': 'right',
+ 'color': beestat.style.color.gray.dark
+ });
+ const sticker_logo_img = document.createElement('img');
+ sticker_logo_img.setAttribute('src', 'img/merchandise/sticker_logo.png');
+ sticker_logo_img.style.height = '64px';
+ sticker_logo_container.appendChild(sticker_logo_img);
+
+ flex_container.appendChild(sticker_logo_container);
+
+ // Buy button
+ const tile_container = document.createElement('div');
+ Object.assign(tile_container.style, {
+ 'flex-grow': '1',
+ 'text-align': 'center'
+ });
+
+ const pay_data = {
+ 'usd': {
+ 'amount': 5,
+ 'payment_link': {
+ 'dev': 'https://buy.stripe.com/test_5kA5nK3dG5yy2L6aF8',
+ 'live': 'https://buy.stripe.com/14k5mXa0N9N13U47sx'
+ }
+ },
+ 'cad': {
+ 'amount': 7,
+ 'payment_link': {
+ 'dev': 'https://buy.stripe.com/test_3csg2og0sgdc71m14w',
+ 'live': 'https://buy.stripe.com/aEUg1B1uh6APbmweV0'
+ }
+ },
+ 'aud': {
+ 'amount': 7,
+ 'payment_link': {
+ 'dev': 'https://buy.stripe.com/test_aEU2bycOg4uugBW14v',
+ 'live': 'https://buy.stripe.com/6oE6r15Kx2kzgGQbIQ'
+ }
+ },
+ 'gbp': {
+ 'amount': 5,
+ 'payment_link': {
+ 'dev': 'https://buy.stripe.com/test_eVaeYkg0s1ii2L66oR',
+ 'live': 'https://buy.stripe.com/28og1B2yl4sH76g6ov'
+ }
+ }
+ };
+
+ const amount_formatted = new Intl.NumberFormat('en-US', {
+ 'style': 'currency',
+ 'currency': beestat.setting('units.currency')
+ }).format(pay_data[beestat.setting('units.currency')].amount);
+
+ const pay_tile = new beestat.component.tile()
+ .set_background_color(beestat.style.color.lightblue.base)
+ .set_background_hover_color(beestat.style.color.lightblue.light)
+ .set_text_color('#fff')
+ .set_size('large')
+ .set_icon('sticker_emoji')
+ .set_text([
+ 'Buy Sticker 2-Pack',
+ amount_formatted
+ ])
+ .render($(tile_container));
+ pay_tile.addEventListener('click', function() {
+ window.open(
+ pay_data[beestat.setting('units.currency')].payment_link[window.environment] +
+ '?prefilled_email=' + beestat.user.get().email_address +
+ '&client_reference_id=' + beestat.user.get().user_id
+ );
+ });
+ flex_container.appendChild(tile_container);
+};
+
+/**
+ * Get the title of the card.
+ *
+ * @return {string} The title.
+ */
+beestat.component.card.merchandise.prototype.get_title_ = function() {
+ return 'Merch!';
+};
diff --git a/js/component/card/settings.js b/js/component/card/settings.js
index 27a3160..c47ce7d 100644
--- a/js/component/card/settings.js
+++ b/js/component/card/settings.js
@@ -80,6 +80,39 @@ beestat.component.card.settings.prototype.decorate_contents_ = function(parent)
distance_radio_group.render(parent);
+ // Currency
+ parent.appendChild(
+ $.createElement('p')
+ .innerText('Currency')
+ );
+ const currency_select = new beestat.component.input.select()
+ .add_option({
+ 'label': 'USD',
+ 'value': 'usd'
+ })
+ .add_option({
+ 'label': 'CAD',
+ 'value': 'cad'
+ })
+ .add_option({
+ 'label': 'AUD',
+ 'value': 'aud'
+ })
+ .add_option({
+ 'label': 'GBP',
+ 'value': 'gpb'
+ });
+
+ currency_select.render(parent);
+
+ currency_select.set_value(beestat.setting('units.currency'));
+
+ currency_select.addEventListener('change', function() {
+ beestat.setting({
+ 'units.currency': currency_select.get_value()
+ });
+ });
+
/**
* Thermosat Summary
*/
diff --git a/js/js.php b/js/js.php
index 405561f..0979067 100755
--- a/js/js.php
+++ b/js/js.php
@@ -89,6 +89,9 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
diff --git a/js/layer/contribute.js b/js/layer/contribute.js
index 596e10a..5d6b7dd 100644
--- a/js/layer/contribute.js
+++ b/js/layer/contribute.js
@@ -34,11 +34,27 @@ beestat.layer.contribute.prototype.decorate_ = function(parent) {
cards.push([
{
'card': new beestat.component.card.contribute(),
- 'size': 9
+ 'size': 8
},
{
- 'card': new beestat.component.card.contribute(),
- 'size': 3
+ 'card': new beestat.component.card.contribute_benefits(),
+ 'size': 4
+ }
+ ]);
+
+ // History
+ cards.push([
+ {
+ 'card': new beestat.component.card.contribute_status(),
+ 'size': 12
+ }
+ ]);
+
+ // Merchandise
+ cards.push([
+ {
+ 'card': new beestat.component.card.merchandise(),
+ 'size': 12
}
]);
diff --git a/js/layer/load.js b/js/layer/load.js
index dc950ab..51d8143 100644
--- a/js/layer/load.js
+++ b/js/layer/load.js
@@ -127,6 +127,13 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
'runtime_thermostat_summary'
);
+ api.add_call(
+ 'stripe_event',
+ 'read_id',
+ {},
+ 'stripe_event'
+ );
+
api.set_callback(function(response) {
beestat.cache.set('user', response.user);
@@ -145,6 +152,7 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
beestat.cache.set('floor_plan', response.floor_plan);
beestat.cache.set('announcement', response.announcement);
beestat.cache.set('runtime_thermostat_summary', response.runtime_thermostat_summary);
+ beestat.cache.set('stripe_event', response.stripe_event);
// Set the active thermostat_id if this is your first time visiting.
if (beestat.setting('thermostat_id') === undefined) {
@@ -203,6 +211,26 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
);
}
+ // Currency (USD is default)
+ const currency_map = {
+ 'CAN': 'cad',
+ 'AUS': 'aud',
+ 'GBR': 'gbp'
+ };
+ if (
+ beestat.setting('units.currency') === undefined &&
+ thermostat.address_id !== null &&
+ beestat.address.is_valid(thermostat.address_id) === true
+ ) {
+ const address = beestat.cache.address[thermostat.address_id];
+ if (currency_map[address.normalized.components.country_iso_3] !== undefined) {
+ beestat.setting(
+ 'units.currency',
+ currency_map[address.normalized.components.country_iso_3]
+ );
+ }
+ }
+
// Rename series if there are multiple stages.
if (beestat.thermostat.get_stages(thermostat.thermostat_id, 'heat') > 1) {
beestat.series.compressor_heat_1.name = 'Heat 1';