Skip to content

Commit

Permalink
Added donation component.
Browse files Browse the repository at this point in the history
  • Loading branch information
imolorhe committed Mar 3, 2018
1 parent 5bcb501 commit 109546a
Show file tree
Hide file tree
Showing 15 changed files with 191 additions and 9 deletions.
16 changes: 16 additions & 0 deletions src/app/actions/donation.ts
@@ -0,0 +1,16 @@
import { Action } from '@ngrx/store';

export const SHOW_DONATION_ALERT = 'SHOW_DONATION_ALERT';
export const HIDE_DONATION_ALERT = 'HIDE_DONATION_ALERT';

export class ShowDonationAlertAction implements Action {
readonly type = SHOW_DONATION_ALERT;
}

export class HideDonationAlertAction implements Action {
readonly type = HIDE_DONATION_ALERT;
}

export type Action =
| ShowDonationAlertAction
| HideDonationAlertAction;
1 change: 1 addition & 0 deletions src/app/app.module.ts
Expand Up @@ -56,6 +56,7 @@ const providers = [
services.QueryService,
services.WindowService,
services.NotifyService,
services.DonationService,
{ provide: ToastOptions, useClass: CustomOption },
reducerProvider
];
Expand Down
4 changes: 4 additions & 0 deletions src/app/config.ts
@@ -1,6 +1,10 @@
import isElectron from './utils/is_electron';

export default {
donation: {
url: 'https://opencollective.com/altair/donate',
action_count_threshold: 50
},
ga: 'UA-41432833-6',
add_query_depth_limit: 3,
max_windows: 10,
Expand Down
11 changes: 11 additions & 0 deletions src/app/containers/app/app.component.html
Expand Up @@ -52,6 +52,17 @@
(languageChange)="onLanguageChange($event)"
></app-settings-dialog>
</div>

<clr-alert [clrAlertType]="'alert-warning'" [clrAlertAppLevel]="true" (clrAlertClosedChange)="hideDonationAlert()" *ngIf="showDonationAlert">
<clr-alert-item>
<span class="alert-text">
Do you find this app useful? Would you mind supporting it's development?
</span>
<div class="alert-actions">
<button class="btn alert-action" (click)="openDonationPage($event)">Donate</button>
</div>
</clr-alert-item>
</clr-alert>
</div>
<!-- The github link has been added to the settings dropdown -->
<!-- <app-fork-repo *ngIf="!isElectron"></app-fork-repo> -->
Expand Down
1 change: 1 addition & 0 deletions src/app/containers/app/app.component.spec.ts
Expand Up @@ -25,6 +25,7 @@ describe('AppComponent', () => {
services.GqlService,
services.DbService,
services.WindowService,
services.DonationService,
{ provide: services.QueryService, useValue: {
loadQuery: () => {},
loadUrl: () => {},
Expand Down
20 changes: 16 additions & 4 deletions src/app/containers/app/app.component.ts
Expand Up @@ -20,10 +20,9 @@ import * as docsActions from '../../actions/docs/docs';
import * as windowsActions from '../../actions/windows/windows';
import * as windowsMetaActions from '../../actions/windows-meta/windows-meta';
import * as settingsActions from '../../actions/settings/settings';
import * as donationActions from '../../actions/donation';

import { QueryService } from '../../services/query.service';
import { GqlService } from '../../services/gql.service';
import { WindowService } from '../../services/window.service';
import { QueryService, GqlService, WindowService, DonationService } from '../../services';

import config from '../../config';

Expand All @@ -40,12 +39,14 @@ export class AppComponent {
activeWindowId = '';
isElectron = isElectron;
isReady = false; // determines if the app is fully loaded. Assets, translations, etc.
showDonationAlert = false;

constructor(
private windowService: WindowService,
private store: Store<fromRoot.State>,
private translate: TranslateService,
private electron: ElectronService
private electron: ElectronService,
private donationService: DonationService
) {
this.settings$ = this.store.select('settings').distinctUntilChanged();

Expand Down Expand Up @@ -85,6 +86,7 @@ export class AppComponent {
.subscribe(data => {
this.windows = data.windows;
this.windowIds = Object.keys(data.windows);
this.showDonationAlert = data.donation.showAlert;

// Set the window IDs in the meta state if it does not already exist
if (data.windowsMeta.windowIds) {
Expand Down Expand Up @@ -195,6 +197,16 @@ export class AppComponent {
}
}

hideDonationAlert() {
this.store.dispatch(new donationActions.HideDonationAlertAction());
}

openDonationPage(e) {
this.donationService.donated();
this.externalLink(e, config.donation.url);
this.hideDonationAlert();
}

externalLink(e, url) {
e.preventDefault();

Expand Down
2 changes: 1 addition & 1 deletion src/app/containers/window/window.component.html
@@ -1,6 +1,6 @@
<div class="content-container">
<div class="content-area app-content-area">
<div class="window-loader" [ngClass]="{ 'window-loader--show': isLoading$ | async }">
<div class="window-loader window-loader--show" [ngClass]="{ 'window-loader--show': isLoading$ | async }">
<div class="window-loader__content">
<img src="assets/img/logo.svg" alt="" class="anim anim-rotate"> {{ 'LOADING_INDICATOR_TEXT' | translate }}
<div class="window-loader__actions">
Expand Down
18 changes: 17 additions & 1 deletion src/app/effects/query.ts
Expand Up @@ -5,7 +5,7 @@ import { Observable } from 'rxjs/Observable';

import * as validUrl from 'valid-url';

import { GqlService, QueryService, NotifyService, DbService } from '../services';
import { GqlService, QueryService, NotifyService, DbService, DonationService } from '../services';
import * as fromRoot from '../reducers';

import { Action as allActions } from '../actions';
Expand All @@ -15,8 +15,11 @@ import * as gqlSchemaActions from '../actions/gql-schema/gql-schema';
import * as dbActions from '../actions/db/db';
import * as docsAction from '../actions/docs/docs';
import * as windowsMetaActions from '../actions/windows-meta/windows-meta';
import * as donationAction from '../actions/donation';

import { downloadJson, downloadData } from '../utils';
import { uaSeedHash } from '../utils/simple_hash';
import config from '../config';

@Injectable()
export class QueryEffects {
Expand Down Expand Up @@ -372,13 +375,26 @@ export class QueryEffects {
return Observable.empty();
});

@Effect()
showDonationAlert$: Observable<Action> = this.actions$
.ofType(queryActions.SEND_QUERY_REQUEST)
.switchMap((data: queryActions.Action) => {
this.donationService.trackAndCheckIfEligible().subscribe(shouldShow => {
if (shouldShow) {
this.store.dispatch(new donationAction.ShowDonationAlertAction());
}
});
return Observable.empty();
});

// Get the introspection after setting the URL
constructor(
private actions$: Actions,
private gqlService: GqlService,
private queryService: QueryService,
private notifyService: NotifyService,
private dbService: DbService,
private donationService: DonationService,
private store: Store<any>
) {}

Expand Down
22 changes: 22 additions & 0 deletions src/app/reducers/donation.ts
@@ -0,0 +1,22 @@
import { Action } from '@ngrx/store';

import * as donation from '../actions/donation';

export interface State {
showAlert: boolean;
}

export const initialState: State = {
showAlert: false
};

export function donationReducer(state = initialState, action: donation.Action): State {
switch (action.type) {
case donation.SHOW_DONATION_ALERT:
return { ...state, showAlert: true };
case donation.HIDE_DONATION_ALERT:
return { ...state, showAlert: false };
default:
return state;
}
}
5 changes: 4 additions & 1 deletion src/app/reducers/index.ts
Expand Up @@ -17,6 +17,7 @@ import * as fromWindows from './windows';
import * as fromHistory from './history/history';
import * as fromWindowsMeta from './windows-meta/windows-meta';
import * as fromSettings from './settings/settings';
import * as fromDonation from './donation';

export interface PerWindowState {
layout: fromLayout.State;
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface State {
windows: fromWindows.State;
windowsMeta: fromWindowsMeta.State;
settings: fromSettings.State;
donation: fromDonation.State;
}

// Meta reducer to log actions
Expand Down Expand Up @@ -72,7 +74,8 @@ export const metaReducers: MetaReducer<any>[] = [
export const reducer: ActionReducerMap<State> = {
windows: fromWindows.windows(combineReducers(perWindowReducers)),
windowsMeta: fromWindowsMeta.windowsMetaReducer,
settings: fromSettings.settingsReducer
settings: fromSettings.settingsReducer,
donation: fromDonation.donationReducer
};

export const reducerToken = new InjectionToken<ActionReducerMap<State>>('Registered Reducers');
Expand Down
3 changes: 2 additions & 1 deletion src/app/services/db.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';

@Injectable()
export class DbService {
Expand All @@ -25,7 +26,7 @@ export class DbService {
getItemByExactKey(key): Observable<any> {
const dbValue = localStorage.getItem(key);

return Observable.create((observer) => {
return Observable.create((observer: Subscriber<any>) => {
if (dbValue) {
try {
const parsedValue = JSON.parse(dbValue);
Expand Down
72 changes: 72 additions & 0 deletions src/app/services/donation.service.ts
@@ -0,0 +1,72 @@
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';

import { DbService } from './';
import config from '../config';
import { uaSeedHash } from '../utils/simple_hash';

@Injectable()
export class DonationService {

private actionCountKey = 'dac';
private seedKey = 'ds';
private hashKey = 'dh';
private seedBuff = 100000;

constructor(
private dbService: DbService
) { }

donated() {
const seed = Math.random() * this.seedBuff;

// Store the seed
this.dbService.setItem(this.seedKey, seed);

// Store the seed hash
this.dbService.setItem(this.hashKey, uaSeedHash(seed));

// Reset the count
this.dbService.setItem(this.actionCountKey, 0);
}

/**
* counts the action and checks if the action is eligible to display the donation alert
*/
trackAndCheckIfEligible(): Observable<boolean> {
/**
* Check if the count threshold has been reached.
* Check if random seed exist
* Check if hash of (seed+ua) matches [donated]
* ~~ Show alert ~~
* Reset counter
*/
const actionCount$ = this.dbService.getItem(this.actionCountKey);
const seed$ = this.dbService.getItem(this.seedKey);
const curHash$ = this.dbService.getItem(this.hashKey);

return Observable.create((obs: Subscriber<boolean>) => {
Observable.combineLatest(actionCount$, seed$, curHash$).subscribe(([actionCount, seed, curHash]) => {
if (actionCount && actionCount >= config.donation.action_count_threshold) {
// Reset count
this.dbService.setItem(this.actionCountKey, 0);

if (seed && uaSeedHash(seed) === curHash) {
// User has donated already
return obs.next(false);
} else {
// User has not donated
return obs.next(true);
}
} else {
// Increment count
this.dbService.setItem(this.actionCountKey, actionCount + 1);

return obs.next(false);
}
});
});
}
}
1 change: 1 addition & 0 deletions src/app/services/index.ts
Expand Up @@ -4,3 +4,4 @@ export { DbService } from './db.service';
export { QueryService } from './query.service';
export { WindowService } from './window.service';
export { NotifyService } from './notify/notify.service';
export { DonationService} from './donation.service';
20 changes: 20 additions & 0 deletions src/app/utils/simple_hash.ts
@@ -0,0 +1,20 @@
export const hash = (s) => {
/* Simple hash function. */
let a = 1, c = 0, h, o;
if (s) {
a = 0;
/*jshint plusplus:false bitwise:false*/
/*tslint:disable */
for (h = s.length - 1; h >= 0; h--) {
o = s.charCodeAt(h);
a = (a << 6&268435455) + o + (o << 14);
c = a & 266338304;
a = c !== 0 ? a ^ c >> 21 : a;
}
}
return String(a);
};

export function uaSeedHash(seed) {
return hash(`${navigator.userAgent}:${seed}`);
};
4 changes: 3 additions & 1 deletion src/scss/_layout.scss
Expand Up @@ -3,8 +3,10 @@
background: $theme-bg-color;
cursor: default;
}
.app-content-area {
.content-container {
position: relative;
}
.app-content-area {
display: flex;
flex-direction: column;
height: 100%;
Expand Down

0 comments on commit 109546a

Please sign in to comment.