Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(typescript): Convert JS to TS #36

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 0 additions & 82 deletions RatingsData.js

This file was deleted.

91 changes: 91 additions & 0 deletions RatingsData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { AsyncStorage } from "react-native";

const keyPrefix = "@RatingRequestData.";
const eventCountKey = keyPrefix + "positiveEventCount";
const ratedTimestamp = keyPrefix + "ratedTimestamp";
const declinedTimestamp = keyPrefix + "declinedTimestamp";

/**
* Private class that let's us interact with AsyncStorage on the device
* @class
*/
class RatingsData {
constructor() {
this.initialize();
}

// Get current count of positive events
async getCount() {
try {
let countString = await AsyncStorage.getItem(eventCountKey);

if (countString !== null) {
return parseInt(countString, 10);
}
} catch (ex) {
console.warn("Couldn't retrieve positive events count. Error:", ex);
}
}

// Increment count of positive events
async incrementCount() {
try {
let currentCount = await this.getCount();

if (typeof currentCount === "undefined") {
return;
}

await AsyncStorage.setItem(eventCountKey, (currentCount + 1).toString());

return currentCount + 1;
} catch (ex) {
console.warn("Could not increment count. Error:", ex);
}
}

async getActionTimestamps() {
try {
let timestamps = await AsyncStorage.multiGet([
ratedTimestamp,
declinedTimestamp
]);

return timestamps;
} catch (ex) {
console.warn("Could not retrieve rated or declined timestamps.", ex);
}
}

async recordDecline() {
try {
await AsyncStorage.setItem(declinedTimestamp, Date.now().toString());
} catch (ex) {
console.warn("Couldn't set declined timestamp.", ex);
}
}

async recordRated() {
try {
await AsyncStorage.setItem(ratedTimestamp, Date.now().toString());
} catch (ex) {
console.warn("Couldn't set rated timestamp.", ex);
}
}

// Initialize keys, if necessary
async initialize() {
try {
let keys = await AsyncStorage.getAllKeys();

if (!keys.some(key => key === eventCountKey)) {
await AsyncStorage.setItem(eventCountKey, "0");
}
} catch (ex) {
// report error or maybe just initialize the values?
console.warn("Uh oh, something went wrong initializing values!", ex);
}
}
}

export default new RatingsData();
122 changes: 79 additions & 43 deletions index.js → index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import React, { Platform, Alert, Linking } from "react-native";
import { Platform, Alert, Linking, AlertButton } from "react-native";

import RatingsData from "./RatingsData";

export const buttonTypes = {
NEUTRAL_DELAY: "NEUTRAL_DELAY",
NEGATIVE_DECLINE: "NEGATIVE_DECLINE",
POSITIVE_ACCEPT: "POSITIVE_ACCEPT"
type ButtonType = "NEUTRAL_DELAY" | "NEGATIVE_DECLINE" | "POSITIVE_ACCEPT";

type RatingRequestorCallback = (
didAppear: boolean,
decision?: "decline" | "delay" | "accept"
) => void;

type RatingRequestorOptions = {
title: string;
message: string;
actionLabels: {
decline: string;
delay: string;
accept: string;
};
buttonOrder: {
ios: ButtonType[];
android: ButtonType[];
};
shouldBoldLastButton: boolean;
storeAppName: string;
storeCountry: string;
timingFunction: (currentCount: number) => boolean;
};

const _config = {
type ButtonDefaults = Record<ButtonType, AlertButton>;

interface Config extends RatingRequestorOptions {
appStoreId: string | null;
}

export enum ButtonTypes {
NEUTRAL_DELAY = "NEUTRAL_DELAY",
NEGATIVE_DECLINE = "NEGATIVE_DECLINE",
POSITIVE_ACCEPT = "POSITIVE_ACCEPT"
}

const _config: Config = {
title: "Rate Me",
message:
"We hope you're loving our app. If you are, would you mind taking a quick moment to leave us a positive review?",
Expand All @@ -18,43 +49,48 @@ const _config = {
delay: "Maybe later...",
accept: "Sure!"
},
timingFunction: function(currentCount) {
timingFunction: function(currentCount: number) {
return (
currentCount > 1 &&
(Math.log(currentCount) / Math.log(3)).toFixed(4) % 1 == 0
parseInt((Math.log(currentCount) / Math.log(3)).toFixed(4)) % 1 == 0
);
},
buttonOrder: {
ios: [
buttonTypes.NEGATIVE_DECLINE,
buttonTypes.NEUTRAL_DELAY,
buttonTypes.POSITIVE_ACCEPT
ButtonTypes.NEGATIVE_DECLINE,
ButtonTypes.NEUTRAL_DELAY,
ButtonTypes.POSITIVE_ACCEPT
],
android: [
buttonTypes.NEGATIVE_DECLINE,
buttonTypes.NEUTRAL_DELAY,
buttonTypes.POSITIVE_ACCEPT
ButtonTypes.NEGATIVE_DECLINE,
ButtonTypes.NEUTRAL_DELAY,
ButtonTypes.POSITIVE_ACCEPT
]
},
shouldBoldLastButton: true,
storeAppName: 'appName',
storeCountry: 'us'
storeAppName: "appName",
storeCountry: "us"
};

async function _isAwaitingRating() {
let timestamps = await RatingsData.getActionTimestamps();

// If no timestamps have been set yet we are still awaiting the user, return true
return timestamps.every(timestamp => {
return timestamp[1] === null;
});
return (
timestamps &&
timestamps.every(timestamp => {
return timestamp[1] === null;
})
);
}

/**
* Creates the RatingRequestor object you interact with
* @class
*/
export default class RatingRequestor {
storeUrl: string;

/**
* @param {string} appStoreId - Required. The ID used in the app's respective app store
* @param {object} options - Optional. Override the defaults. Takes the following shape, with all elements being optional:
Expand All @@ -66,29 +102,31 @@ export default class RatingRequestor {
* delay: {string},
* accept: {string}
* },
* buttonOrder: {
* ios: [buttonTypes],
* android: [buttonTypes],
* }
* buttonOrder: {
* ios: [buttonTypes],
* android: [buttonTypes],
* }
* shouldBoldLastButton: {boolean},
* storeAppName: {string},
* storeCountry: {string},
* timingFunction: {func}
* }
*/
constructor(appStoreId, options) {
constructor(appStoreId: string, options: Partial<RatingRequestorOptions>) {
// Check for required options
if (!appStoreId) {
throw "You must specify your app's store ID on construction to use the Rating Requestor.";
}

// Merge defaults with user-supplied config
Object.assign(_config, options);
_config.appStoreId = appStoreId;
_config.appStoreId = appStoreId;

this.storeUrl = Platform.select({
ios: `https://itunes.apple.com/${_config.storeCountry}/app/${_config.storeAppName}/id${_config.appStoreId}`,
android: `market://details?id=${_config.appStoreId}`,
this.storeUrl = Platform.select({
ios: `https://itunes.apple.com/${_config.storeCountry}/app/${
_config.storeAppName
}/id${_config.appStoreId}`,
android: `market://details?id=${_config.appStoreId}`
});
}

Expand All @@ -100,8 +138,8 @@ export default class RatingRequestor {
*
* @param {function(didAppear: boolean, result: string)} callback Optional. Callback that reports whether the dialog appeared and what the result was.
*/
showRatingDialog(callback = () => {}) {
const buttonDefaults = {
showRatingDialog(callback: RatingRequestorCallback = () => {}) {
const buttonDefaults: ButtonDefaults = {
NEGATIVE_DECLINE: {
text: _config.actionLabels.decline,
onPress: () => {
Expand All @@ -122,21 +160,19 @@ export default class RatingRequestor {
callback(true, "accept");
Linking.openURL(this.storeUrl);
},
style: "default",
style: "default"
}
};
};

const buttons = Platform.select(_config.buttonOrder).map(bo => buttonDefaults[bo]);
const buttons = Platform.select(_config.buttonOrder).map(
(bo: ButtonType) => buttonDefaults[bo]
);

if (_config.shouldBoldLastButton) {
buttons[2].style = 'cancel';
}
if (_config.shouldBoldLastButton) {
buttons[2].style = "cancel";
}

Alert.alert(
_config.title,
_config.message,
buttons,
);
Alert.alert(_config.title, _config.message, buttons);
}

/**
Expand All @@ -145,11 +181,11 @@ export default class RatingRequestor {
*
* @param {function(didAppear: boolean, result: string)} callback Optional. Callback that reports whether the dialog appeared and what the result was.
*/
async handlePositiveEvent(callback = () => {}) {
async handlePositiveEvent(callback: RatingRequestorCallback = () => {}) {
if (await _isAwaitingRating()) {
let currentCount = await RatingsData.incrementCount();

if (_config.timingFunction(currentCount)) {
if (currentCount && _config.timingFunction(currentCount)) {
this.showRatingDialog(callback);
} else callback(false);
} else callback(false);
Expand Down
Loading