Skip to content
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
84 changes: 84 additions & 0 deletions storybook/stories/FlipCreditCard/CardView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from "react";
import { StyleProp, ViewStyle } from "react-native";
import {
defaultCVCPlaceholder,
defaultExpiryPlaceholder,
defaultCardHolderNamePlaceholder,
defaultCreditCardNumberPlaceholder
} from "./common";
import CreditCardViewFront from "./CardViewFront";
import CreditCardViewBack from "./CardViewBack";

export interface CreditCardLabels {
expiryDate: string;
}

export interface CreditCardFields {
creditCardName: string;
creditCardNumber: string;
expiryDate: string;
cvc: string;
}

export interface Props {
style?: StyleProp<ViewStyle>;
scale?: number;
creditCardName?: string;
creditCardNumber?: string;
creditCardNumberLength?: number;
expiryDate?: string;
cvc?: string;
placeholders?: Partial<CreditCardFields>;
localizedLabels?: Partial<CreditCardFields>;
visibleFace: "front" | "back";
}

export default function CardView(props: Props) {
const {
creditCardName,
creditCardNumber,
cvc,
placeholders: optionalPlaceholders,
localizedLabels: optionalLabels,
expiryDate,
visibleFace,
style: externalStyle
} = props;

const placeholders = {
cvc: defaultCVCPlaceholder,
expiryDate: defaultExpiryPlaceholder,
creditCardName: defaultCardHolderNamePlaceholder,
creditCardNumber: defaultCreditCardNumberPlaceholder,
...optionalPlaceholders
};

const labels = {
expiryDate: "MM/YY",
...optionalLabels
};

if (visibleFace === "front") {
return (
<CreditCardViewFront
style={externalStyle}
cardHolderName={creditCardName}
number={creditCardNumber}
expiryDate={expiryDate}
placeholders={placeholders}
labels={labels}
/>
);
} else {
return (
<CreditCardViewBack
style={externalStyle}
cvc={cvc}
expiryDate={expiryDate}
cardHolderName={creditCardName}
placeholders={placeholders}
labels={labels}
/>
);
}
}
54 changes: 54 additions & 0 deletions storybook/stories/FlipCreditCard/CardViewBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import { View, Text, StyleProp, ViewStyle } from "react-native";
import style, {
CardBackgroundColorStart,
CardBackgroundColorEnd
} from "./style";
import { CardInformationContainer } from "./CardViewFront";
import { LinearGradient } from "expo";
import { addPlaceholderStyleIfNeeded } from "./common";
import { CreditCardFields, CreditCardLabels } from "./CardView";

interface CreditCardViewBackProps {
cvc: string;
cardHolderName: string;
expiryDate: string;
style?: StyleProp<ViewStyle>;
placeholders: CreditCardFields;
labels: CreditCardLabels;
}

export default function CreditCardViewBack(props: CreditCardViewBackProps) {
const {
cvc,
cardHolderName,
expiryDate,
style: externalStyle,
placeholders,
labels
} = props;

return (
<LinearGradient
colors={[CardBackgroundColorStart, CardBackgroundColorEnd]}
style={[style.cardView, externalStyle]}
>
<View style={style.cardViewBackStrip} />
<View style={style.cardViewBackCVCContainer}>
<Text
style={[style.cardViewBackCVCText, addPlaceholderStyleIfNeeded(cvc)]}
>
{cvc || placeholders.cvc}
</Text>
</View>
<CardInformationContainer
style={style.cardInformationContainerReverse}
cardHolderName={cardHolderName}
expiryDate={expiryDate}
textStyle={style.cardInformationContainerReverseText}
placeholders={placeholders}
labels={labels}
/>
</LinearGradient>
);
}
126 changes: 126 additions & 0 deletions storybook/stories/FlipCreditCard/CardViewFront.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
import {
View,
Text,
Image,
StyleProp,
ViewStyle,
TextStyle
} from "react-native";
import { LinearGradient } from "expo";
import style, {
CardBackgroundColorEnd,
CardBackgroundColorStart
} from "./style";
import { addPlaceholderStyleIfNeeded } from "./common";
import { CreditCardFields, CreditCardLabels } from "./CardView";

interface CreditCardViewFrontProps {
cardHolderName: string;
number: string;
expiryDate: string;
style?: StyleProp<ViewStyle>;
placeholders: CreditCardFields;
labels: CreditCardLabels;
}

interface CreditCardInfoContainerProps {
cardHolderName: string;
expiryDate: string;
textStyle?: StyleProp<TextStyle>;
style?: StyleProp<ViewStyle>;
placeholders: CreditCardFields;
labels: CreditCardLabels;
}

export function CardInformationContainer(props: CreditCardInfoContainerProps) {
const {
cardHolderName,
expiryDate,
textStyle,
style: externalStyle,
placeholders,
labels
} = props;

return (
<View style={[style.cardInformationContainer, externalStyle]}>
<Text
style={[
style.cardViewText,
style.cardViewFrontCardNameText,
textStyle,
addPlaceholderStyleIfNeeded(cardHolderName)
]}
>
{(cardHolderName || placeholders.creditCardName).toUpperCase()}
</Text>
<View style={style.cardViewFrontExpiration}>
<Text
style={[
style.cardViewText,
style.cardViewFrontExpiryLabel,
textStyle
]}
>
{labels.expiryDate}
</Text>
<Text
style={[
style.cardViewText,
textStyle,
addPlaceholderStyleIfNeeded(expiryDate)
]}
>
{expiryDate || placeholders.expiryDate}
</Text>
</View>
</View>
);
}

export default function CreditCardViewFront(props: CreditCardViewFrontProps) {
const {
cardHolderName,
number,
expiryDate,
placeholders,
style: externalStyle,
labels
} = props;

return (
<LinearGradient
colors={[CardBackgroundColorStart, CardBackgroundColorEnd]}
style={[style.cardView, externalStyle]}
>
<Image
style={style.cardViewFrontBackground}
source={require("./assets/gray-world-map-md.png")}
/>
<Image
style={style.cardViewFrontChip}
source={require("./assets/card-chip.png")}
/>
<Image
style={style.cardViewFrontBrandIcon}
source={require("./assets/visa-icon.png")}
/>
<CardInformationContainer
cardHolderName={cardHolderName}
expiryDate={expiryDate}
placeholders={placeholders}
labels={labels}
/>
<Text
style={[
style.cardViewText,
style.cardViewFrontNumberText,
addPlaceholderStyleIfNeeded(number)
]}
>
{number || placeholders.creditCardNumber}
</Text>
</LinearGradient>
);
}
113 changes: 113 additions & 0 deletions storybook/stories/FlipCreditCard/FlipOnGesture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from "react";
import CardView, { Props as CardViewProps } from "./CardView";
import {
Animated,
TouchableOpacity,
View,
PanResponder,
Dimensions
} from "react-native";
import style from "./style";

interface Props extends CardViewProps {}

type Face = "front" | "back";

interface State {
visibleFace: Face;
}

const screenWidth = Dimensions.get("window").width;

export default class FlipCreditCard extends React.PureComponent<Props, State> {
state = { visibleFace: "front" as Face };
prevX = 0;
rotation = new Animated.Value(0);
currentRotation = 0;

_panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

onPanResponderGrant: (evt, gestureState) => {
// The gesture has started. Show visual feedback so the user knows
// what is happening!
// gestureState.d{x,y} will be set to zero now
this.prevX = 0;
},
onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
this.currentRotation = Math.max(
0,
Math.min(
1,
this.currentRotation + (gestureState.dx - this.prevX) / screenWidth
)
);

this.prevX = gestureState.dx;

if (this.currentRotation >= 0.5) {
this.setState({ visibleFace: "back" });
} else {
this.setState({ visibleFace: "front" });
}

this.rotation.setValue(this.currentRotation);
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// Returns whether this component should block native components from becoming the JS
// responder. Returns true by default. Is currently only supported on android.
return true;
},
onPanResponderEnd: (evt, gestureState) => {
this.prevX = 0;
Animated.spring(this.rotation, {
friction: 4,
toValue: this.currentRotation >= 0.5 ? 1 : 0
}).start();
}
});

nextSide: (face: Face) => Face = face => {
switch (face) {
case "front":
return "back";
case "back":
return "front";
}
};

render() {
const { scale = 1 } = this.props;
const { visibleFace } = this.state;

const rotation = this.rotation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "180deg"]
});

return (
<View {...this._panResponder.panHandlers}>
<Animated.View
style={[
style.container,
{
transform: [
{ rotateY: rotation },
{ scale },
{ perspective: 1000 }
]
}
]}
>
<CardView {...this.props} visibleFace={visibleFace} />
</Animated.View>
</View>
);
}
}
Loading