Skip to content

Commit 3f798ae

Browse files
authored
docs: add payment iframe (#126)
1 parent afb63da commit 3f798ae

3 files changed

Lines changed: 360 additions & 2 deletions

File tree

docs/install.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ sidebar_position: 1
33
description: 'Install `@react-native-google-signin/google-signin`. Covers paid (Universal Sign In) and free (Original) versions, requirements, and package manager setup.'
44
---
55

6-
# Installation
6+
import CreemEmbeddedCheckout from '@site/src/components/CreemEmbeddedCheckout';
77

8-
> We recommend [Expo](https://expo.dev) and [EAS](https://expo.dev/eas) for building and deploying your React Native app. Expo offers the best developer experience and is well-supported by this library.
8+
# Installation
99

1010
The recommended option is [Universal Sign In](https://universal-sign-in.com) (paid). A free legacy version is also available — see [below](#public-version-free) for the differences. If you are an EAS customer, you may be able to access the paid version for free, [learn more](https://forms.gle/tpP7TfUGW1CwgaEZ8).
1111

1212
Why paid? According to the [State of React Native Survey](https://results.2024.stateofreactnative.com/en-US/opinions/#opinions_pain_points_multiple), unmaintained packages are **the #1 pain point** of the React Native ecosystem. Your purchase enables the module to be rock-solid, and contributions to upstream SDKs such as [1](https://github.com/openid/AppAuth-iOS/pull/788), [2](https://github.com/google/GoogleSignIn-iOS/pull/402), [3](https://github.com/googlesamples/google-services/issues/426), [4](https://github.com/google/GoogleSignIn-iOS/issues/457), [5](https://issuetracker.google.com/issues/424210681), [6](https://issuetracker.google.com/issues/474817166).
1313

1414
[//]: # '🌟'
1515

16+
<CreemEmbeddedCheckout checkoutUrl="https://www.creem.io/checkout/prod_2zElFBVDXGigzZTiK4MvYX/ch_5PmaTQdtYUlCLt08HF0bkE" />
17+
1618
### Universal Sign In (premium) {/* #premium */}
1719

1820
⭐️ **Key Features**:
@@ -166,3 +168,5 @@ The latest version of the Universal Sign In package supports (use older versions
166168
| ------------ | --------------- |
167169
| expo | 52.0.40 - 56 |
168170
| react-native | 0.76.0 - 0.86 |
171+
172+
> We recommend [Expo](https://expo.dev) and [EAS](https://expo.dev/eas) for building and deploying your React Native app. Expo offers the best developer experience and is well-supported by this library.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import React, { useEffect, useId, useRef, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import './styles.css';
4+
5+
type CreemEmbeddedCheckoutProps = {
6+
checkoutUrl: string;
7+
};
8+
9+
const CREEM_ORIGIN = 'https://www.creem.io';
10+
11+
export default function CreemEmbeddedCheckout({
12+
checkoutUrl,
13+
}: CreemEmbeddedCheckoutProps) {
14+
const [isOpen, setIsOpen] = useState(false);
15+
const [shouldLoadCheckout, setShouldLoadCheckout] = useState(false);
16+
const titleId = useId();
17+
const closeButtonRef = useRef<HTMLButtonElement>(null);
18+
const iframeRef = useRef<HTMLIFrameElement>(null);
19+
const modalContentRef = useRef<HTMLDivElement>(null);
20+
21+
const loadCheckout = () => {
22+
setShouldLoadCheckout(true);
23+
};
24+
25+
const openCheckout = () => {
26+
loadCheckout();
27+
setIsOpen(true);
28+
};
29+
30+
useEffect(() => {
31+
if (document.querySelector(`link[rel="preconnect"][href="${CREEM_ORIGIN}"]`)) {
32+
return;
33+
}
34+
35+
const link = document.createElement('link');
36+
link.href = CREEM_ORIGIN;
37+
link.rel = 'preconnect';
38+
link.crossOrigin = 'anonymous';
39+
document.head.appendChild(link);
40+
}, []);
41+
42+
useEffect(() => {
43+
if (!isOpen) {
44+
return;
45+
}
46+
47+
const previousOverflow = document.body.style.overflow;
48+
const previousActiveElement = document.activeElement;
49+
const getFocusableElements = () =>
50+
Array.from(
51+
modalContentRef.current?.querySelectorAll<HTMLElement>(
52+
'button, iframe, [data-focus-sentinel]',
53+
) ?? [],
54+
).filter(
55+
(element) =>
56+
!element.hasAttribute('disabled') &&
57+
element.getAttribute('aria-hidden') !== 'true',
58+
);
59+
60+
const onKeyDown = (event: KeyboardEvent) => {
61+
if (event.key === 'Escape') {
62+
setIsOpen(false);
63+
return;
64+
}
65+
66+
if (event.key !== 'Tab') {
67+
return;
68+
}
69+
70+
if (
71+
event.shiftKey &&
72+
document.activeElement === closeButtonRef.current
73+
) {
74+
event.preventDefault();
75+
iframeRef.current?.focus();
76+
return;
77+
}
78+
79+
const focusableElements = getFocusableElements();
80+
const firstElement = focusableElements[0];
81+
const lastElement = focusableElements[focusableElements.length - 1];
82+
83+
if (!firstElement || !lastElement) {
84+
event.preventDefault();
85+
return;
86+
}
87+
88+
if (event.shiftKey && document.activeElement === firstElement) {
89+
event.preventDefault();
90+
lastElement.focus();
91+
return;
92+
}
93+
94+
if (!event.shiftKey && document.activeElement === lastElement) {
95+
event.preventDefault();
96+
firstElement.focus();
97+
}
98+
};
99+
100+
document.addEventListener('keydown', onKeyDown);
101+
document.body.style.overflow = 'hidden';
102+
closeButtonRef.current?.focus();
103+
104+
return () => {
105+
document.removeEventListener('keydown', onKeyDown);
106+
document.body.style.overflow = previousOverflow;
107+
108+
if (previousActiveElement instanceof HTMLElement) {
109+
previousActiveElement.focus();
110+
}
111+
};
112+
}, [isOpen]);
113+
114+
return (
115+
<section className="creem-embed" aria-labelledby="creem-embed-title">
116+
<div className="creem-embed__header">
117+
<div>
118+
<p className="creem-embed__eyebrow">Universal Sign In license</p>
119+
<h2 id="creem-embed-title">Ready to install Universal Sign In?</h2>
120+
<p>
121+
Buy a license, then use the private npm registry setup below to add
122+
the package to your app.
123+
</p>
124+
</div>
125+
<button
126+
className="button button--primary creem-embed__button"
127+
type="button"
128+
onClick={openCheckout}
129+
onFocus={loadCheckout}
130+
onPointerEnter={loadCheckout}
131+
>
132+
Buy license
133+
</button>
134+
</div>
135+
{shouldLoadCheckout &&
136+
createPortal(
137+
<div
138+
className={`creem-embed-modal ${
139+
isOpen ? '' : 'creem-embed-modal--preload'
140+
}`}
141+
role={isOpen ? 'dialog' : undefined}
142+
aria-modal={isOpen ? 'true' : undefined}
143+
aria-labelledby={isOpen ? titleId : undefined}
144+
aria-hidden={!isOpen}
145+
>
146+
<button
147+
className="creem-embed-modal__backdrop"
148+
type="button"
149+
aria-label="Close embedded checkout"
150+
onClick={() => setIsOpen(false)}
151+
/>
152+
<div className="creem-embed-modal__content" ref={modalContentRef}>
153+
<div className="creem-embed-modal__header">
154+
<div>
155+
<p className="creem-embed-modal__eyebrow">Secure checkout</p>
156+
<h2 id={titleId}>Complete your purchase</h2>
157+
</div>
158+
<button
159+
className="creem-embed-modal__close"
160+
ref={closeButtonRef}
161+
type="button"
162+
aria-label="Close embedded checkout"
163+
onClick={() => setIsOpen(false)}
164+
/>
165+
</div>
166+
<span
167+
className="creem-embed-modal__sentinel"
168+
data-focus-sentinel
169+
tabIndex={0}
170+
onFocus={() => iframeRef.current?.focus()}
171+
/>
172+
<iframe
173+
className="creem-embed-modal__frame"
174+
ref={iframeRef}
175+
src={checkoutUrl}
176+
title="Universal Sign In checkout"
177+
allow="payment *; publickey-credentials-get *"
178+
/>
179+
<span
180+
className="creem-embed-modal__sentinel"
181+
data-focus-sentinel
182+
tabIndex={0}
183+
onFocus={() => closeButtonRef.current?.focus()}
184+
/>
185+
</div>
186+
</div>,
187+
document.body,
188+
)}
189+
</section>
190+
);
191+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
.creem-embed {
2+
margin: 2rem 0;
3+
padding: 1rem;
4+
border: 1px solid var(--ifm-color-emphasis-300);
5+
border-radius: 8px;
6+
background: var(--ifm-background-surface-color);
7+
}
8+
9+
.creem-embed__header {
10+
display: flex;
11+
align-items: flex-start;
12+
justify-content: space-between;
13+
gap: 1rem;
14+
}
15+
16+
.creem-embed__button {
17+
flex-shrink: 0;
18+
}
19+
20+
.creem-embed__eyebrow,
21+
.creem-embed-modal__eyebrow {
22+
margin: 0 0 0.25rem;
23+
color: var(--ifm-color-primary);
24+
font-size: 0.78rem;
25+
font-weight: 700;
26+
letter-spacing: 0.04em;
27+
text-transform: uppercase;
28+
}
29+
30+
.creem-embed__header h2 {
31+
max-width: 32rem;
32+
margin: 0 0 0.5rem;
33+
}
34+
35+
.creem-embed__header p {
36+
margin: 0;
37+
}
38+
39+
.creem-embed-modal {
40+
position: fixed;
41+
inset: 0;
42+
z-index: 9999;
43+
display: grid;
44+
place-items: center;
45+
padding: 1rem 0;
46+
}
47+
48+
.creem-embed-modal--preload {
49+
visibility: hidden;
50+
opacity: 0;
51+
pointer-events: none;
52+
}
53+
54+
.creem-embed-modal__backdrop {
55+
position: absolute;
56+
inset: 0;
57+
border: 0;
58+
background: rgba(15, 23, 42, 0.68);
59+
cursor: pointer;
60+
}
61+
62+
.creem-embed-modal__content {
63+
position: relative;
64+
z-index: 1;
65+
display: flex;
66+
flex-direction: column;
67+
width: min(100%, 580px);
68+
max-height: min(900px, calc(100vh - 2rem));
69+
overflow: hidden;
70+
border-radius: 8px;
71+
background: var(--ifm-background-surface-color);
72+
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
73+
}
74+
75+
.creem-embed-modal__header {
76+
display: flex;
77+
align-items: center;
78+
justify-content: space-between;
79+
gap: 1rem;
80+
padding: 0.75rem 1rem;
81+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
82+
}
83+
84+
.creem-embed-modal__header h2 {
85+
margin: 0;
86+
font-size: 1rem;
87+
}
88+
89+
.creem-embed-modal__close {
90+
position: relative;
91+
flex-shrink: 0;
92+
width: 2.25rem;
93+
height: 2.25rem;
94+
border: 1px solid var(--ifm-color-emphasis-300);
95+
border-radius: 999px;
96+
background: var(--ifm-background-surface-color);
97+
color: var(--ifm-font-color-base);
98+
cursor: pointer;
99+
transition:
100+
background 160ms ease,
101+
border-color 160ms ease,
102+
box-shadow 160ms ease,
103+
transform 160ms ease;
104+
}
105+
106+
.creem-embed-modal__close::before,
107+
.creem-embed-modal__close::after {
108+
position: absolute;
109+
top: 50%;
110+
left: 50%;
111+
width: 0.9rem;
112+
height: 2px;
113+
border-radius: 999px;
114+
background: currentColor;
115+
content: '';
116+
}
117+
118+
.creem-embed-modal__close::before {
119+
transform: translate(-50%, -50%) rotate(45deg);
120+
}
121+
122+
.creem-embed-modal__close::after {
123+
transform: translate(-50%, -50%) rotate(-45deg);
124+
}
125+
126+
.creem-embed-modal__close:hover,
127+
.creem-embed-modal__close:focus-visible {
128+
border-color: var(--ifm-color-primary);
129+
background: var(--ifm-color-emphasis-100);
130+
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.18);
131+
outline: none;
132+
}
133+
134+
.creem-embed-modal__close:hover {
135+
transform: translateY(-1px);
136+
}
137+
138+
.creem-embed-modal__frame {
139+
align-self: center;
140+
width: 100%;
141+
height: min(820px, calc(100vh - 6.5rem));
142+
border: 0;
143+
background: #ffffff;
144+
}
145+
146+
.creem-embed-modal__sentinel {
147+
position: absolute;
148+
width: 1px;
149+
height: 1px;
150+
overflow: hidden;
151+
clip: rect(0 0 0 0);
152+
white-space: nowrap;
153+
}
154+
155+
@media (max-width: 700px) {
156+
.creem-embed__header {
157+
flex-direction: column;
158+
}
159+
160+
.creem-embed__button {
161+
width: 100%;
162+
}
163+
}

0 commit comments

Comments
 (0)