RN 0.75 and libs upgrade

This commit is contained in:
minibits-cash
2024-09-17 00:52:26 +02:00
parent f7a170af58
commit bb8995c717
234 changed files with 54953 additions and 14007 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

View File

@@ -1,4 +1,11 @@
module.exports = { module.exports = {
root: true, root: true,
extends: '@react-native', extends: '@react-native',
}; rules: {
semi: ['error', 'never'],
"unused-imports/no-unused-imports": "error"
},
plugins: ['unused-imports']
}

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

153
.github/workflows/jsCodePush-PROD.yaml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Minibits JS bundle code push
# This workflow is manually triggered. Only if package.json version has changed it creates .env file,
# builds new js bundle and pushes it to devices running app with TEST env and STAGING code push deployment key.
# At the end it creates and pushes new version tag to github.
on: workflow_dispatch
jobs:
deploy-code-push:
runs-on: macos-latest
permissions:
id-token: write
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 18
# Fetch javascript bundle version from package.json
- name: Read package.json version and detect changes
id: version-check
uses: salsify/action-detect-and-tag-new-version@v2
with:
create-tag: false
- name: Set versions
run: |-
echo "PREV_JS_VERSION=${{steps.version-check.outputs.previous-version }}" >> $GITHUB_ENV
echo "CURRENT_JS_VERSION=${{steps.version-check.outputs.current-version }}" >> $GITHUB_ENV
# Codepush build will continue only if version changed from previous commit - TEMP SKIP CHECK
- name: Detect version change
run: |
if [ "${{ env.PREV_JS_VERSION }}" != "${{ env.CURRENT_JS_VERSION }}" ]; then
echo "IS_VERSION_CHANGED=true" >> $GITHUB_ENV
echo "Detected version change"
else
echo "IS_VERSION_CHANGED=true" >> $GITHUB_ENV
echo "No version change, skipping further processing"
fi
# Fetch native app versionName and versionCode from app/build.gradle
- name: Get Android native version
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
id: version-reader-android
uses: michpohl/android-expose-version-name-action@v1.0.0
with:
path: android/app/build.gradle
expose-version-name: 'true' # sets ANDROID_VERSION_NAME to env
# Get commit hash
- name: Get commit hash
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
id: commit-hash
uses: pr-mpt/actions-commit-hash@v2
- name: Set commit hash
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
echo "COMMIT_HASH=${{steps.commit-hash.outputs.short }}" >> $GITHUB_ENV
# Get commit message
- name: Get last commit message
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
});
const lastCommit = response.data.commit;
const message = lastCommit.message;
console.log(message);
core.exportVariable('COMMIT_MESSAGE', message);
# Create .env file
- name: Generate .env
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
echo APP_ENV='PROD' >> .env
echo SENTRY_DSN='${{ secrets.SENTRY_DSN }}' >> .env
echo CODEPUSH_PRODUCTION_DEPLOYMENT_KEY='${{ secrets.CODEPUSH_PRODUCTION_DEPLOYMENT_KEY }}' >> .env
echo NATIVE_VERSION_ANDROID='${{ env.ANDROID_VERSION_NAME }}' >> .env
echo JS_BUNDLE_VERSION='${{ env.CURRENT_JS_VERSION }}' >> .env
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
echo MINIBIT_SERVER_NOSTR_PUBKEY='${{ secrets.MINIBIT_SERVER_NOSTR_PUBKEY }}' >> .env
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash' >> .env
echo MINIBITS_MINT_URL='https://mint.minibits.cash/Bitcoin' >> .env
echo COMMIT='${{ env.COMMIT_HASH }}' >> .env
# Install dependecies from package.json
- name: Install app dependencies
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
uses: bahmutov/npm-install@v1.10.2
# with:
# install-command: 'yarn install'
- name: Install Sentry CLI
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: npm install @sentry/cli --legacy-peer-deps
# Code push with source maps export for Sentry
- name: Install AppCenter CLI
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: npm install -g appcenter-cli
- name: Deploy to CodePush Android
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: appcenter codepush release-react -a minibits-cash/minibits_wallet_android -d Production --description '${{ env.COMMIT_MESSAGE }} (v${{ env.CURRENT_JS_VERSION }})' --sourcemap-output ./build/index.android.bundle.map --output-dir ./build
env:
APPCENTER_ACCESS_TOKEN: ${{ secrets.APPCENTER_ACCESS_TOKEN_ANDROID }}
# Upload source files to Sentry
- name: Upload source files to Sentry
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
./node_modules/.bin/sentry-cli login --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }}
./node_modules/.bin/sentry-cli releases \
files minibits_wallet_android@${{ env.CURRENT_JS_VERSION }} \
upload-sourcemaps \
--strip-prefix /build \
--dist ${{ env.ANDROID_VERSION_NAME }} \
--org ${{ secrets.SENTRY_ORG }} \
--project minibits-wallet \
./build/index.android.bundle.map ./build/Codepush/index.android.bundle
# Create the release tag in github repo
- name: Create tag
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tagPrefix = "v";
const tagName = tagPrefix + process.env.CURRENT_JS_VERSION;
github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/tags/${tagName}`,
sha: context.sha
});

153
.github/workflows/jsCodePush-TEST.yaml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Minibits JS bundle code push
# This workflow is manually triggered. Only if package.json version has changed it creates .env file,
# builds new js bundle and pushes it to devices running app with TEST env and STAGING code push deployment key.
# At the end it creates and pushes new version tag to github.
on: workflow_dispatch
jobs:
deploy-code-push:
runs-on: macos-latest
permissions:
id-token: write
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: actions/setup-node@v3
with:
node-version: 18
# Fetch javascript bundle version from package.json
- name: Read package.json version and detect changes
id: version-check
uses: salsify/action-detect-and-tag-new-version@v2
with:
create-tag: false
- name: Set versions
run: |-
echo "PREV_JS_VERSION=${{steps.version-check.outputs.previous-version }}" >> $GITHUB_ENV
echo "CURRENT_JS_VERSION=${{steps.version-check.outputs.current-version }}" >> $GITHUB_ENV
# Codepush build will continue only if version changed from previous commit
- name: Detect version change
run: |
if [ "${{ env.PREV_JS_VERSION }}" != "${{ env.CURRENT_JS_VERSION }}" ]; then
echo "IS_VERSION_CHANGED=true" >> $GITHUB_ENV
echo "Detected version change"
else
echo "IS_VERSION_CHANGED=false" >> $GITHUB_ENV
echo "No version change, skipping further processing"
fi
# Fetch native app versionName and versionCode from app/build.gradle
- name: Get Android native version
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
id: version-reader-android
uses: michpohl/android-expose-version-name-action@v1.0.0
with:
path: android/app/build.gradle
expose-version-name: 'true' # sets ANDROID_VERSION_NAME to env
# Get commit hash
- name: Get commit hash
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
id: commit-hash
uses: pr-mpt/actions-commit-hash@v2
- name: Set commit hash and native version information
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
echo "COMMIT_HASH=${{steps.commit-hash.outputs.short }}" >> $GITHUB_ENV
# Get commit message
- name: Get last commit message
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
});
const lastCommit = response.data.commit;
const message = lastCommit.message;
console.log(message);
core.exportVariable('COMMIT_MESSAGE', message);
# Create .env file
- name: Generate .env
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
echo APP_ENV='TEST' >> .env
echo SENTRY_DSN='${{ secrets.SENTRY_DSN }}' >> .env
echo CODEPUSH_STAGING_DEPLOYMENT_KEY='${{ secrets.CODEPUSH_STAGING_DEPLOYMENT_KEY }}' >> .env
echo NATIVE_VERSION_ANDROID='${{ env.ANDROID_VERSION_NAME }}' >> .env
echo JS_BUNDLE_VERSION='${{ env.CURRENT_JS_VERSION }}' >> .env
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
echo MINIBIT_SERVER_NOSTR_PUBKEY='${{ secrets.MINIBIT_SERVER_NOSTR_PUBKEY }}' >> .env
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash/test' >> .env
echo MINIBITS_MINT_URL='https://mint.minibits.cash/Bitcoin' >> .env
echo COMMIT='${{ env.COMMIT_HASH }}' >> .env
# Install dependecies from package.json
- name: Install app dependencies
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
uses: bahmutov/npm-install@v1.10.2
# with:
# install-command: 'yarn install'
- name: Install Sentry CLI
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: npm install @sentry/cli --legacy-peer-deps
# Code push with source maps export for Sentry
- name: Install AppCenter CLI
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: npm install -g appcenter-cli
- name: Deploy to CodePush Android
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: appcenter codepush release-react -a minibits-cash/minibits_wallet_android -d Staging --description '${{ env.COMMIT_MESSAGE }} (v${{ env.CURRENT_JS_VERSION }})' --sourcemap-output ./build/index.android.bundle.map --output-dir ./build
env:
APPCENTER_ACCESS_TOKEN: ${{ secrets.APPCENTER_ACCESS_TOKEN_ANDROID }}
# Upload source files to Sentry
- name: Upload source files to Sentry
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
run: |-
./node_modules/.bin/sentry-cli login --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }}
./node_modules/.bin/sentry-cli releases \
files minibits_wallet_android@${{ env.CURRENT_JS_VERSION }} \
upload-sourcemaps \
--strip-prefix /build \
--dist ${{ env.NATIVE_VERSION }} \
--org ${{ secrets.SENTRY_ORG }} \
--project minibits-wallet \
./build/index.android.bundle.map ./build/Codepush/index.android.bundle
# Create the release tag in github repo
- name: Create tag
if: ${{ env.IS_VERSION_CHANGED == 'true' }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tagPrefix = "v";
const tagName = tagPrefix + process.env.CURRENT_JS_VERSION;
github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/tags/${tagName}`,
sha: context.sha
});

View File

@@ -4,4 +4,5 @@ module.exports = {
bracketSpacing: false, bracketSpacing: false,
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
semi: false,
}; };

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"inlang.vs-code-extension"
]
}

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"[javascriptreact][typescriptreact]": {
"editor.insertSpaces": true,
"editor.tabSize": 2
},
"[javascript][typescript]": {
"editor.insertSpaces": true,
"editor.tabSize": 2
}
}

BIN
.yarn/releases/yarn-3.6.4.cjs vendored Normal file

Binary file not shown.

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.6.4.cjs

118
App.tsx
View File

@@ -1,118 +0,0 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from 'react';
import type {PropsWithChildren} from 'react';
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
type SectionProps = PropsWithChildren<{
title: string;
}>;
function Section({children, title}: SectionProps): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
}
function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<Header />
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
});
export default App;

9
LICENSE.md Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Minibits.cash
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

246
README.md
View File

@@ -1,79 +1,207 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). ![feature_sharp](https://github.com/minibits-cash/minibits_wallet/assets/138401554/2c615363-fbf6-4a9e-ac89-9228ae159cda)
# Getting Started
>**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. # Disclaimer
## Step 1: Start the Metro Server ⚠️ If you are using this app, please take the following into consideration:
- This wallet should be used for research purposes only.
- The wallet is a beta version with incomplete functionality and both known and unknown bugs.
- Do not use it with large amounts of ecash.
- The ecash stored in the wallet is issued by the mint. You trust the mint to back it with bitcoin until you transfer your holdings to another bitcoin lightning wallet.
- The Cashu protocol that the wallet implements has not yet received extensive review or testing.
First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native.
To start Metro, run the following command from the _root_ of your React Native project: # Minibits Wallet
Minibits is an ecash and lightning wallet with a focus on ease of use and security. Ecash is issued by mints and backed by Bitcoin via the [Cashu](https://cashu.space) protocol and Lightning Network. Ecash is cash-like yet digital token with cheap and instant transfers and high privacy guarantees.
## Roadmap
Platform support
- [x] Android app
- [ ] iOS app
- [x] Light and dark mode
- [x] i18n support
- [ ] Other then EN languange support
Mints
- [x] Multiple currency units issued by mints [✨ New!]
- [x] Add multiple mints
- [x] Remove mints
- [x] Block receiving from mint
- [x] Show mint balances grouped by currency units [✨ New!]
- [x] Handle mint keys rotation (not tested)
- [x] Mint status and information screen
Receive ecash
- [x] Scan QR code of a ecash token
- [x] Paste ecash token from the clipboard
- [x] Notification on received payment (app needs to be in foreground)
- [x] Receive Nostr zaps or Lightning payments to minibits.cash address
- [x] Receive ecash from another wallet over NOSTR message sent to minibits.cash address
- [x] Receive ecash in person while being offline, redeem later (MVP version)
- [x] Realtime and encrypted push notifications on receive to minibits.cash lightning address [✨ New!]
Send ecash
- [x] Share ecash token to send through another app
- [x] Show ecash token as a QR code
- [x] Notification on payment received by the payee (app needs to come to foreground)
- [x] Send ecash to contact (minibits.cash or another NOSTR address)
- [ ] Lock ecash sent offline to the receiver wallet key
Top up wallet
- [x] Show QR code with bitcoin Lightning invoice to pay
- [x] Share encoded bitcoin Lightning invoice to pay
- [x] Share payment request with contact over NOSTR message
- [x] Top up balance with LNURL Withdraw
Pay / Cash out from wallet
- [x] One click ZAPS - tip users of NOSTR social network
- [x] Paste or scan and settle bitcoin Lightning invoice with your ecash
- [x] Pay payment request received from another contact
- [x] Pay to LNURL Pay static links / codes
- [x] Pay to Lightning address
- [ ] Transfer (swap) ecash to another mint
Transaction history
- [x] Unified transaction history for all kinds of transactions
- [x] Audit trail of transaction events
- [x] Filter pending transactions
- [x] Retry after recoverable transaction errors [✨ New!]
- [ ] Revert pending transaction in 1 click (get back tokens not claimed by receiver)
- [ ] Tags and related filtering of transactions
- [x] Delete incomplete and failed transactions from history
Contacts
- [x] Private contacts address book for payments
- [x] Public contacts (followed users on NOSTR social network) for tipping and donations
- [x] Load public contacts from custom NOSTR relay
- [x] Wallet addresses as random public NOSTR addresses (random123@minibits.cash)
- [x] Custom wallet names (myname@minibits.cash)
- [x] Wallet addresses usable as Lightning addresses to receive payments from many Lightning wallets
- [x] Private contacts with other than minibits.cash NOSTR adresses and relays
Backup and recovery
- [x] Local append-only backup of all ecash in a database separate from wallet storage
- [x] Recovery tool to recover ecash from local backup
- [x] Recover wallet in case spent ecash remain in the wallet due to an exception during a transaction
- [x] Off-device backup and recovery using 12 words menmonic phrase
- [x] Retry transaction after recoverable errors [✨ New!]
- [x] Auto-recover funds if wallet failed to receive ecash issued by mint due to network or device failure
- [x] Smooth migration to another device - recovery of wallet address without balances using mnemonic phrase [✨ New!]
Interoperability
- [x] Nostr Wallet Connect - lets you initiate payments from another app, such as Nostr client [✨ New!]
- [x] Deeplinks - app reacts to lightning: and cashu: URIs
Security and Privacy
- [x] Optional AES encryption of wallet storage using a key stored in the device secure key storage
- [x] Use device biometry to login (if storage encryption is on)
- [ ] Connect to the mints on .onion Tor addresses using own Tor daemon [discontinued from v0.1.7]
- [x] Connect to the mints on .onion Tor addresses using Orbot [✨ New!]
Self-funding
- [X] Donation for custom wallet name
DevOps
- [x] OTA updates (opt in)
- [ ] Automated tests
- [ ] Automated release pipelines for both OTA updates and native releases
## Architecture
The wallet's design has been crafted to prioritize the following primary quality properties:
- Support both Android and iOS mobile platforms
- Achieve fast UX and startup time (despite using React Native)
- Minimize the risk of data/ecash loss
- Bring ecash UX on par with the current standard of traditional finance (tradfi) mobile apps
As a result, the following architectural constraints are in place:
- Wherever available, use libraries with a fast JSI (JavaScript Interface) to native modules.
- Avoid Expo modules.
- Use fastest available storage for most wallet operations and separate local database storage to store data that incrementally grows.
- Leverage local database as an append-only ecash backup independent from fast storage.
<img src="https://www.minibits.cash/img/minibits_architecture_v2.png">
Open architectural concepts that were still open for discussion when the wallet had been released
- [x] Contacts management - identities, sharing contacts, send ecash with the UX of tradfi instant payment while keeping privacy towards mints - Implemented as NOSTR keypairs and NIP05 public sharable names that ecash can be sent to
- [x] Off-device backup strategy - Implemented using @gandlafbtc concept of deterministic secrets
- [ ] UX and naming conventions - ecash is not always intuitive. UX for new users heavily depends on using the right abstractions or terms to describe what is going on. This wallet wants to serve as a means to test what could work. One of the first ideas is to avoid terms such as token or proof and propose the term --coin ++ecash instead.
- [ ] Suitable Tor daemon available to replace not maintained react-native-tor. From v0.1.8-beta.33 connection through Orbot in VPN mode is possible.
## Download and test
Minibits wallet is in early beta and available as of now only for Android devices. You have the following options to try it out:
- [x] Download it from Google Play
- [x] Join testing program on Google Play to get early releases to test (Submit your email to get an invite on [Minibits.cash](https://www.minibits.cash))
- [x] Download .apk file from Releases page and install it on your phone
# Development
Minibits is a bare React Native app written in Typescript. The project structure and code itself are intentionally verbose to support readability. Critical wallet code is reasonably documented. However, there is vast space for existing code improvements, refactoring, and bug fixing. This is an early beta software and the author does not code for a living.
The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operations is in place and its use to encrypt storage is opt-in.
Wallet state is managed by mobx-state-tree and persisted in fast MMKV storage. Only the basic mobx concepts are in place, whole model could be improved. All critical wallet code is in services/walletService.ts and all ecash state changes are in models/ProofsStore.ts. Wallet communication with the mints is in model/Wallet.ts and uses [cashu-ts](https://github.com/cashubtc/cashu-ts) library.
Crypto operations are handled by react-native-quick-crypto, that is fast and does not require awful javascript shims. Transaction history and ecash backup is stored in sqlite, with fast react-native-quick-sqlite driver that enables to run lighter queries synchronously.
Wallet included own Tor daemon using react-native-tor library to connect to the mints over Tor network. However this seems not to be long term approach as this library is
not properly maintained and future updates of React native will likely break it. Help with replacement would be appreciated.
In case of breaking state and data model changes, versioning and code is ready to run necessary migrations on wallet startup.
# Running in development mode
To run Minibits wallet in dev mode, set up the React Native development environment and the Yarn package manager. Then clone this repository, navigate to the minibits_wallet directory, and run the following:
```bash ```bash
# using npm yarn install
npm start ```
# OR using Yarn There are post-install patches to some of the libraries that should run automatically and are necessary for a successful run. See the patches directory for more info.
After the dependecies are installed, continue to create the following .env file in the root folder:
```bash
APP_ENV='DEV'
MINIBITS_SERVER_API_KEY='mockkey'
MINIBITS_SERVER_API_HOST='http://localhost/api'
MINIBITS_NIP05_DOMAIN='@localhost'
MINIBITS_RELAY_URL='ws://localhost/relay'
MINIBITS_MINT_URL='http://localhost/mint'
```
Local NOSTR address and Lighnting brigde server are not necessary to run the wallet.
Then make sure you have the Android device connected by running:
```bash
yarn adb
```
Finally run this and pray:
```bash
yarn start yarn start
``` ```
## Step 2: Start your Application In case of issues, repo includes commits history from the out of the box react native app up until the complete wallet. You can see build.gradle and other changes one by one and hopefully figure out what's wrong.
Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: # Building
### For Android Create debug .apk:
```bash ```bash
# using npm yarn android:dev
npm run android
# OR using Yarn
yarn android
``` ```
### For iOS # Automated testing
```bash The app has the scaffolding for automated tests; they are yet to be implemented. For functional bugs or suggestions please raise an issue.
# using npm
npm run ios
# OR using Yarn # Contributing
yarn ios
```
If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. Contributions are welcome, just start and we will figure out what's next.
This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively.
## Step 3: Modifying your App
Now that you have successfully run the app, let's modify it.
1. Open `App.tsx` in your text editor of choice and edit some lines.
2. For **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Developer Menu** (<kbd>Ctrl</kbd> + <kbd>M</kbd> (on Window and Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (on macOS)) to see your changes!
For **iOS**: Hit <kbd>Cmd ⌘</kbd> + <kbd>R</kbd> in your iOS Simulator to reload the app and see your changes!
## Congratulations! :tada:
You've successfully run and modified your React Native App. :partying_face:
### Now what?
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started).
# Troubleshooting
If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
# Learn More
To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.

View File

@@ -4,7 +4,7 @@
import 'react-native'; import 'react-native';
import React from 'react'; import React from 'react';
import App from '../App'; import App from '../src/App';
// Note: import explicitly to use the types shipped with jest. // Note: import explicitly to use the types shipped with jest.
import {it} from '@jest/globals'; import {it} from '@jest/globals';
@@ -13,5 +13,5 @@ import {it} from '@jest/globals';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
it('renders correctly', () => { it('renders correctly', () => {
renderer.create(<App />); renderer.create(<App appName='minibits' />);
}); });

View File

@@ -0,0 +1,24 @@
const fs = require('fs');
const path = require('path');
const messagesFolder = path.resolve(__dirname, '../src/i18n_messages');
for (const file of fs.readdirSync(messagesFolder)) {
const filePath = path.join(messagesFolder, file);
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('${')) {
const index = line.indexOf('${');
const neighborValue = 10
const neighbor = line
.trim()
.slice(
Math.max(0, index - neighborValue),
Math.min(line.length, index + neighborValue),
)
console.log(`${filePath}(${i+1},${index+1}): Likely incorrect placeholder selector \$\{\}: ...${neighbor}...`);
}
}
}

View File

@@ -0,0 +1,16 @@
const { exec } = require('child_process');
exec('tsc --noEmit', (error, stdout, stderr) => {
if (error) {
const output = stdout;
const filteredErrors = output.split('\n').filter(line => line.includes('TxKeyPath'));
if (filteredErrors.length > 0) {
console.log(filteredErrors.join('\n'));
} else {
console.log('No errors containing TxKeyPath found.');
}
// process.exit(1); // Ensure the script returns an error code if there were any errors
} else {
console.log('No TypeScript errors found.');
}
});

View File

@@ -1,6 +1,10 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android" apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react" apply plugin: "com.facebook.react"
apply plugin: "com.google.gms.google-services"
import com.android.build.OutputFile
/** /**
* This is the configuration block to customize your React Native Android app. * This is the configuration block to customize your React Native Android app.
@@ -72,20 +76,47 @@ def enableProguardInReleaseBuilds = false
*/ */
def jscFlavor = 'org.webkit:android-jsc:+' def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Private function to get the list of Native Architectures you want to build.
* This reads the value from reactNativeArchitectures in your gradle.properties
* file and works together with the --active-arch-only flag of react-native run-android.
*/
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
namespace "com.minibits_wallet_075" namespace "com.minibits_wallet"
defaultConfig { defaultConfig {
applicationId "com.minibits_wallet_075" applicationId "com.minibits_wallet"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 58001
versionName "1.0" versionName "0.1.9"
}
splits {
abi {
reset()
enable true
universalApk true // If true, also generate a universal APK
include (*reactNativeArchitectures())
}
} }
signingConfigs { signingConfigs {
release {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
storeFile file(project.property('MYAPP_UPLOAD_STORE_FILE'))
storePassword project.property('MYAPP_UPLOAD_STORE_PASSWORD')
keyAlias project.property('MYAPP_UPLOAD_KEY_ALIAS')
keyPassword project.property('MYAPP_UPLOAD_KEY_PASSWORD')
}
}
debug { debug {
storeFile file('debug.keystore') storeFile file('debug.keystore')
storePassword 'android' storePassword 'android'
@@ -94,22 +125,52 @@ android {
} }
} }
buildTypes { buildTypes {
debug {
signingConfig signingConfigs.debug
}
release { release {
// Caution! In production, you need to generate your own keystore file. // Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android. // see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
} }
debug {
signingConfig signingConfigs.debug
}
}
// minibits_wallet:build
packagingOptions {
pickFirst 'lib/arm64-v8a/liblog.so'
pickFirst 'lib/x86/liblog.so'
pickFirst 'lib/x86_64/liblog.so'
pickFirst 'lib/armeabi-v7a/liblog.so'
pickFirst 'lib/arm64-v8a/liblog.so'
pickFirst 'lib/arm64-v8a/libcrypto.so'
pickFirst 'lib/x86/libcrypto.so'
pickFirst 'lib/x86_64/libcrypto.so'
pickFirst 'lib/armeabi-v7a/libcrypto.so'
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
}
}
} }
} }
dependencies { dependencies {
// The version of react-native is set by the React Native Gradle Plugin // The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android") implementation("com.facebook.react:react-android")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
if (hermesEnabled.toBoolean()) { if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android") implementation("com.facebook.react:hermes-android")
@@ -117,3 +178,6 @@ dependencies {
implementation jscFlavor implementation jscFlavor
} }
} }
// code-push
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"

View File

@@ -1,26 +1,57 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false"
android:allowBackup="false" android:theme="@style/AppTheme"
android:theme="@style/AppTheme" android:supportsRtl="true"
android:supportsRtl="true"> android:usesCleartextTraffic="true"
<activity >
android:name=".MainActivity" <activity
android:label="@string/app_name" android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:label="@string/app_name"
android:launchMode="singleTask" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:windowSoftInputMode="adjustResize" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity>
</application> <intent-filter>
</manifest> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lightning" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lnurlw" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lnurlp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="cashu" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

View File

@@ -1,5 +1,7 @@
package com.minibits_wallet_075 package com.minibits_wallet
// minibits_wallet:react-native-screens
import android.os.Bundle
import com.facebook.react.ReactActivity import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
@@ -11,7 +13,7 @@ class MainActivity : ReactActivity() {
* Returns the name of the main component registered from JavaScript. This is used to schedule * Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component. * rendering of the component.
*/ */
override fun getMainComponentName(): String = "minibits_wallet_075" override fun getMainComponentName(): String = "minibits_wallet"
/** /**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
@@ -19,4 +21,9 @@ class MainActivity : ReactActivity() {
*/ */
override fun createReactActivityDelegate(): ReactActivityDelegate = override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
// minibits_wallet:react-native-screens
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
} }

View File

@@ -1,4 +1,4 @@
package com.minibits_wallet_075 package com.minibits_wallet
import android.app.Application import android.app.Application
import com.facebook.react.PackageList import com.facebook.react.PackageList
@@ -10,16 +10,17 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import com.microsoft.codepush.react.CodePush
class MainApplication : Application(), ReactApplication { class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) { object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> = override fun getPackages(): List<ReactPackage> {
PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example:
// Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage());
// add(MyReactNativePackage()) return PackageList(this).packages
} }
override fun getJSMainModuleName(): String = "index" override fun getJSMainModuleName(): String = "index"
@@ -27,6 +28,14 @@ class MainApplication : Application(), ReactApplication {
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
// minibits_wallet:react-native-code-push
// 2. Override the getJSBundleFile method in order to let
// the CodePush runtime determine where to get the JS
// bundle location from on each app start
override fun getJSBundleFile(): String {
return CodePush.getJSBundleFile()
}
} }
override val reactHost: ReactHost override val reactHost: ReactHost
@@ -38,6 +47,6 @@ class MainApplication : Application(), ReactApplication {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() load()
} }
} }
} }

View File

@@ -17,8 +17,7 @@
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material" android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material" android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material" android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material" android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
>
<selector> <selector>
<!-- <!--

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">minibits_wallet_075</string> <string name="app_name">Minibits</string>
<string moduleConfig="true" name="CodePushDeploymentKey">mockkey</string><!-- real key passed to codepush in .sync method -->
</resources> </resources>

View File

@@ -1,7 +1,7 @@
buildscript { buildscript {
ext { ext {
buildToolsVersion = "34.0.0" buildToolsVersion = "34.0.0"
minSdkVersion = 23 minSdkVersion = 24
compileSdkVersion = 34 compileSdkVersion = 34
targetSdkVersion = 34 targetSdkVersion = 34
ndkVersion = "26.1.10909125" ndkVersion = "26.1.10909125"
@@ -15,6 +15,13 @@ buildscript {
classpath("com.android.tools.build:gradle") classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin") classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
classpath("com.google.gms:google-services:4.4.1")
}
}
// minibits_wallet:react-native-camera-kit
allprojects {
repositories {
google()
} }
} }

View File

@@ -1,6 +1,8 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") } plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'minibits_wallet_075' rootProject.name = 'minibits_wallet'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin') includeBuild('../node_modules/@react-native/gradle-plugin')
// code-push
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')

View File

@@ -1,4 +1,4 @@
{ {
"name": "minibits_wallet_075", "name": "minibits_wallet",
"displayName": "minibits_wallet_075" "displayName": "Minibits"
} }

Binary file not shown.

BIN
assets/icons/nostr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,3 +1,17 @@
module.exports = { module.exports = {
presets: ['module:@react-native/babel-preset'], presets: ['module:@react-native/babel-preset'],
plugins: [
[
'module-resolver',
{
alias: {
'crypto': 'react-native-quick-crypto',
'stream': 'stream-browserify',
'buffer': '@craftzdog/react-native-buffer',
},
},
],
'module:react-native-dotenv',
'react-native-reanimated/plugin'
],
}; };

View File

@@ -1,9 +1,22 @@
/** import 'react-native-reanimated' // needed for qrcode reader
* @format import { install } from 'react-native-quick-crypto' // needed for secp256k1, conf in babel.config
*/ install()
import 'react-native-url-polyfill/auto' // URL.host etc
import 'text-encoding-polyfill' // needed in cashu-ts
import {AppRegistry} from 'react-native'
import messaging from '@react-native-firebase/messaging';
import App from './src/App'
import {name as appName} from './app.json'
import { NotificationService } from './src/services/notificationService';
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App); function BootstrapApp() {
return <App appName={appName} />
}
// Setup notification listeners and handlers
messaging().onMessage(NotificationService.onForegroundNotification)
messaging().setBackgroundMessageHandler(NotificationService.onBackgroundNotification)
AppRegistry.registerComponent(appName, () => BootstrapApp)

View File

@@ -14,7 +14,7 @@ if linkage != nil
use_frameworks! :linkage => linkage.to_sym use_frameworks! :linkage => linkage.to_sym
end end
target 'minibits_wallet_075' do target 'minibits_wallet' do
config = use_native_modules! config = use_native_modules!
use_react_native!( use_react_native!(
@@ -23,7 +23,7 @@ target 'minibits_wallet_075' do
:app_path => "#{Pod::Config.instance.installation_root}/.." :app_path => "#{Pod::Config.instance.installation_root}/.."
) )
target 'minibits_wallet_075Tests' do target 'minibits_walletTests' do
inherit! :complete inherit! :complete
# Pods for testing # Pods for testing
end end

View File

@@ -7,12 +7,12 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* minibits_wallet_075Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* minibits_wallet_075Tests.m */; }; 00E356F31AD99517003FC87E /* minibits_walletTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* minibits_walletTests.m */; };
0C80B921A6F3F58F76C31292 /* libPods-minibits_wallet_075.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet_075.a */; }; 0C80B921A6F3F58F76C31292 /* libPods-minibits_wallet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet.a */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
7699B88040F8A987B510C191 /* libPods-minibits_wallet_075-minibits_wallet_075Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet_075-minibits_wallet_075Tests.a */; }; 7699B88040F8A987B510C191 /* libPods-minibits_wallet-minibits_walletTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet-minibits_walletTests.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -22,28 +22,28 @@
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
remoteInfo = minibits_wallet_075; remoteInfo = minibits_wallet;
}; };
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
00E356EE1AD99517003FC87E /* minibits_wallet_075Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = minibits_wallet_075Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356EE1AD99517003FC87E /* minibits_walletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = minibits_walletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* minibits_wallet_075Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = minibits_wallet_075Tests.m; sourceTree = "<group>"; }; 00E356F21AD99517003FC87E /* minibits_walletTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = minibits_walletTests.m; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* minibits_wallet_075.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = minibits_wallet_075.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* minibits_wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = minibits_wallet.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = minibits_wallet_075/AppDelegate.h; sourceTree = "<group>"; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = minibits_wallet/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = minibits_wallet_075/AppDelegate.mm; sourceTree = "<group>"; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = minibits_wallet/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = minibits_wallet_075/Images.xcassets; sourceTree = "<group>"; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = minibits_wallet/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = minibits_wallet_075/Info.plist; sourceTree = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = minibits_wallet/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = minibits_wallet_075/main.m; sourceTree = "<group>"; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = minibits_wallet/main.m; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = minibits_wallet_075/PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = minibits_wallet/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet_075-minibits_wallet_075Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-minibits_wallet_075-minibits_wallet_075Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet-minibits_walletTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-minibits_wallet-minibits_walletTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3B4392A12AC88292D35C810B /* Pods-minibits_wallet_075.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet_075.debug.xcconfig"; path = "Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075.debug.xcconfig"; sourceTree = "<group>"; }; 3B4392A12AC88292D35C810B /* Pods-minibits_wallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet.debug.xcconfig"; path = "Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet.debug.xcconfig"; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-minibits_wallet_075.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet_075.release.xcconfig"; path = "Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075.release.xcconfig"; sourceTree = "<group>"; }; 5709B34CF0A7D63546082F79 /* Pods-minibits_wallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet.release.xcconfig"; path = "Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet.release.xcconfig"; sourceTree = "<group>"; };
5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet_075-minibits_wallet_075Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet_075-minibits_wallet_075Tests.debug.xcconfig"; path = "Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests.debug.xcconfig"; sourceTree = "<group>"; }; 5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet-minibits_walletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet-minibits_walletTests.debug.xcconfig"; path = "Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests.debug.xcconfig"; sourceTree = "<group>"; };
5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet_075.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-minibits_wallet_075.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-minibits_wallet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = minibits_wallet_075/LaunchScreen.storyboard; sourceTree = "<group>"; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = minibits_wallet/LaunchScreen.storyboard; sourceTree = "<group>"; };
89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet_075-minibits_wallet_075Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet_075-minibits_wallet_075Tests.release.xcconfig"; path = "Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests.release.xcconfig"; sourceTree = "<group>"; }; 89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet-minibits_walletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minibits_wallet-minibits_walletTests.release.xcconfig"; path = "Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests.release.xcconfig"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -52,7 +52,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7699B88040F8A987B510C191 /* libPods-minibits_wallet_075-minibits_wallet_075Tests.a in Frameworks */, 7699B88040F8A987B510C191 /* libPods-minibits_wallet-minibits_walletTests.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -60,20 +60,20 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0C80B921A6F3F58F76C31292 /* libPods-minibits_wallet_075.a in Frameworks */, 0C80B921A6F3F58F76C31292 /* libPods-minibits_wallet.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
00E356EF1AD99517003FC87E /* minibits_wallet_075Tests */ = { 00E356EF1AD99517003FC87E /* minibits_walletTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
00E356F21AD99517003FC87E /* minibits_wallet_075Tests.m */, 00E356F21AD99517003FC87E /* minibits_walletTests.m */,
00E356F01AD99517003FC87E /* Supporting Files */, 00E356F01AD99517003FC87E /* Supporting Files */,
); );
path = minibits_wallet_075Tests; path = minibits_walletTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
00E356F01AD99517003FC87E /* Supporting Files */ = { 00E356F01AD99517003FC87E /* Supporting Files */ = {
@@ -84,7 +84,7 @@
name = "Supporting Files"; name = "Supporting Files";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
13B07FAE1A68108700A75B9A /* minibits_wallet_075 */ = { 13B07FAE1A68108700A75B9A /* minibits_wallet */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */,
@@ -95,15 +95,15 @@
13B07FB71A68108700A75B9A /* main.m */, 13B07FB71A68108700A75B9A /* main.m */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
); );
name = minibits_wallet_075; name = minibits_wallet;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet_075.a */, 5DCACB8F33CDC322A6C60F78 /* libPods-minibits_wallet.a */,
19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet_075-minibits_wallet_075Tests.a */, 19F6CBCC0A4E27FBF8BF4A61 /* libPods-minibits_wallet-minibits_walletTests.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -118,9 +118,9 @@
83CBB9F61A601CBA00E9B192 = { 83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07FAE1A68108700A75B9A /* minibits_wallet_075 */, 13B07FAE1A68108700A75B9A /* minibits_wallet */,
832341AE1AAA6A7D00B99B32 /* Libraries */, 832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* minibits_wallet_075Tests */, 00E356EF1AD99517003FC87E /* minibits_walletTests */,
83CBBA001A601CBA00E9B192 /* Products */, 83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */, 2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */, BBD78D7AC51CEA395F1C20DB /* Pods */,
@@ -133,8 +133,8 @@
83CBBA001A601CBA00E9B192 /* Products */ = { 83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07F961A680F5B00A75B9A /* minibits_wallet_075.app */, 13B07F961A680F5B00A75B9A /* minibits_wallet.app */,
00E356EE1AD99517003FC87E /* minibits_wallet_075Tests.xctest */, 00E356EE1AD99517003FC87E /* minibits_walletTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -142,10 +142,10 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = { BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3B4392A12AC88292D35C810B /* Pods-minibits_wallet_075.debug.xcconfig */, 3B4392A12AC88292D35C810B /* Pods-minibits_wallet.debug.xcconfig */,
5709B34CF0A7D63546082F79 /* Pods-minibits_wallet_075.release.xcconfig */, 5709B34CF0A7D63546082F79 /* Pods-minibits_wallet.release.xcconfig */,
5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet_075-minibits_wallet_075Tests.debug.xcconfig */, 5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet-minibits_walletTests.debug.xcconfig */,
89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet_075-minibits_wallet_075Tests.release.xcconfig */, 89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet-minibits_walletTests.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -153,9 +153,9 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
00E356ED1AD99517003FC87E /* minibits_wallet_075Tests */ = { 00E356ED1AD99517003FC87E /* minibits_walletTests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "minibits_wallet_075Tests" */; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "minibits_walletTests" */;
buildPhases = ( buildPhases = (
A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */, A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */, 00E356EA1AD99517003FC87E /* Sources */,
@@ -169,14 +169,14 @@
dependencies = ( dependencies = (
00E356F51AD99517003FC87E /* PBXTargetDependency */, 00E356F51AD99517003FC87E /* PBXTargetDependency */,
); );
name = minibits_wallet_075Tests; name = minibits_walletTests;
productName = minibits_wallet_075Tests; productName = minibits_walletTests;
productReference = 00E356EE1AD99517003FC87E /* minibits_wallet_075Tests.xctest */; productReference = 00E356EE1AD99517003FC87E /* minibits_walletTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
13B07F861A680F5B00A75B9A /* minibits_wallet_075 */ = { 13B07F861A680F5B00A75B9A /* minibits_wallet */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minibits_wallet_075" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minibits_wallet" */;
buildPhases = ( buildPhases = (
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
@@ -190,9 +190,9 @@
); );
dependencies = ( dependencies = (
); );
name = minibits_wallet_075; name = minibits_wallet;
productName = minibits_wallet_075; productName = minibits_wallet;
productReference = 13B07F961A680F5B00A75B9A /* minibits_wallet_075.app */; productReference = 13B07F961A680F5B00A75B9A /* minibits_wallet.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@@ -212,7 +212,7 @@
}; };
}; };
}; };
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minibits_wallet_075" */; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minibits_wallet" */;
compatibilityVersion = "Xcode 12.0"; compatibilityVersion = "Xcode 12.0";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
@@ -225,8 +225,8 @@
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
13B07F861A680F5B00A75B9A /* minibits_wallet_075 */, 13B07F861A680F5B00A75B9A /* minibits_wallet */,
00E356ED1AD99517003FC87E /* minibits_wallet_075Tests */, 00E356ED1AD99517003FC87E /* minibits_walletTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -273,15 +273,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = {
@@ -299,7 +299,7 @@
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-minibits_wallet_075-minibits_wallet_075Tests-checkManifestLockResult.txt", "$(DERIVED_FILE_DIR)/Pods-minibits_wallet-minibits_walletTests-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@@ -321,7 +321,7 @@
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-minibits_wallet_075-checkManifestLockResult.txt", "$(DERIVED_FILE_DIR)/Pods-minibits_wallet-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@@ -334,15 +334,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
@@ -351,15 +351,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075/Pods-minibits_wallet_075-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet/Pods-minibits_wallet-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = {
@@ -368,15 +368,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet_075-minibits_wallet_075Tests/Pods-minibits_wallet_075-minibits_wallet_075Tests-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minibits_wallet-minibits_walletTests/Pods-minibits_wallet-minibits_walletTests-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@@ -386,7 +386,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00E356F31AD99517003FC87E /* minibits_wallet_075Tests.m in Sources */, 00E356F31AD99517003FC87E /* minibits_walletTests.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -404,7 +404,7 @@
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
00E356F51AD99517003FC87E /* PBXTargetDependency */ = { 00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 13B07F861A680F5B00A75B9A /* minibits_wallet_075 */; target = 13B07F861A680F5B00A75B9A /* minibits_wallet */;
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
@@ -412,14 +412,14 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = { 00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet_075-minibits_wallet_075Tests.debug.xcconfig */; baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-minibits_wallet-minibits_walletTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1", "DEBUG=1",
"$(inherited)", "$(inherited)",
); );
INFOPLIST_FILE = minibits_wallet_075Tests/Info.plist; INFOPLIST_FILE = minibits_walletTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -433,17 +433,17 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/minibits_wallet_075.app/minibits_wallet_075"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/minibits_wallet.app/minibits_wallet";
}; };
name = Debug; name = Debug;
}; };
00E356F71AD99517003FC87E /* Release */ = { 00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet_075-minibits_wallet_075Tests.release.xcconfig */; baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-minibits_wallet-minibits_walletTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
INFOPLIST_FILE = minibits_wallet_075Tests/Info.plist; INFOPLIST_FILE = minibits_walletTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -457,19 +457,19 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/minibits_wallet_075.app/minibits_wallet_075"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/minibits_wallet.app/minibits_wallet";
}; };
name = Release; name = Release;
}; };
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-minibits_wallet_075.debug.xcconfig */; baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-minibits_wallet.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = minibits_wallet_075/Info.plist; INFOPLIST_FILE = minibits_wallet/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -481,7 +481,7 @@
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = minibits_wallet_075; PRODUCT_NAME = minibits_wallet;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -490,12 +490,12 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-minibits_wallet_075.release.xcconfig */; baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-minibits_wallet.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = minibits_wallet_075/Info.plist; INFOPLIST_FILE = minibits_wallet/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -507,7 +507,7 @@
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = minibits_wallet_075; PRODUCT_NAME = minibits_wallet;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@@ -655,7 +655,7 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "minibits_wallet_075Tests" */ = { 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "minibits_walletTests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
00E356F61AD99517003FC87E /* Debug */, 00E356F61AD99517003FC87E /* Debug */,
@@ -664,7 +664,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minibits_wallet_075" */ = { 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minibits_wallet" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */, 13B07F941A680F5B00A75B9A /* Debug */,
@@ -673,7 +673,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minibits_wallet_075" */ = { 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minibits_wallet" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA201A601CBA00E9B192 /* Debug */,

View File

@@ -15,9 +15,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A" BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minibits_wallet_075.app" BuildableName = "minibits_wallet.app"
BlueprintName = "minibits_wallet_075" BlueprintName = "minibits_wallet"
ReferencedContainer = "container:minibits_wallet_075.xcodeproj"> ReferencedContainer = "container:minibits_wallet.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -33,9 +33,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E" BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "minibits_wallet_075Tests.xctest" BuildableName = "minibits_walletTests.xctest"
BlueprintName = "minibits_wallet_075Tests" BlueprintName = "minibits_walletTests"
ReferencedContainer = "container:minibits_wallet_075.xcodeproj"> ReferencedContainer = "container:minibits_wallet.xcodeproj">
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
</Testables> </Testables>
@@ -55,9 +55,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A" BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minibits_wallet_075.app" BuildableName = "minibits_wallet.app"
BlueprintName = "minibits_wallet_075" BlueprintName = "minibits_wallet"
ReferencedContainer = "container:minibits_wallet_075.xcodeproj"> ReferencedContainer = "container:minibits_wallet.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
@@ -72,9 +72,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A" BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minibits_wallet_075.app" BuildableName = "minibits_wallet.app"
BlueprintName = "minibits_wallet_075" BlueprintName = "minibits_wallet"
ReferencedContainer = "container:minibits_wallet_075.xcodeproj"> ReferencedContainer = "container:minibits_wallet.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -6,7 +6,7 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
self.moduleName = @"minibits_wallet_075"; self.moduleName = @"minibits_wallet";
// You can add your custom initial props in the dictionary below. // You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native. // They will be passed down to the ViewController used by React Native.
self.initialProps = @{}; self.initialProps = @{};

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>minibits_wallet_075</string> <string>minibits_wallet</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

View File

@@ -16,7 +16,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="minibits_wallet_075" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb"> <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="minibits_wallet" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
<rect key="frame" x="0.0" y="202" width="375" height="43"/> <rect key="frame" x="0.0" y="202" width="375" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>

View File

@@ -7,11 +7,11 @@
#define TIMEOUT_SECONDS 600 #define TIMEOUT_SECONDS 600
#define TEXT_TO_LOOK_FOR @"Welcome to React" #define TEXT_TO_LOOK_FOR @"Welcome to React"
@interface minibits_wallet_075Tests : XCTestCase @interface minibits_walletTests : XCTestCase
@end @end
@implementation minibits_wallet_075Tests @implementation minibits_walletTests
- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
{ {

1
minibits.inlang/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cache

View File

@@ -0,0 +1 @@
bd9b37e0c87069e40a8b6f5b819efc075600ab5f8016e4041da309066262dbca

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en",
"languageTags": [
"en",
"sk"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
"https://cdn.jsdelivr.net/gh/KraXen72/inlang-t-function-jsx-hybrid-matcher/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-json@latest/dist/index.js"
],
"plugin.inlang.json": {
"pathPattern": "./src/i18n_messages/{languageTag}.json",
"variableReferencePattern": [
"%{",
"}"
]
},
"plugin.minibits.inlangmatcher": {
"preferredTfuncName": "translate",
"recognizedTfuncNames": [
"translate"
],
"recognizedJSXAttributes": [
"tx",
"subTx",
"contentTx"
]
}
}

13659
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,82 @@
{ {
"name": "minibits_wallet_075", "name": "minibits_wallet",
"version": "0.0.1", "version": "0.1.9-beta",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android:clean": "cd android && ./gradlew clean",
"android:dev": "npx react-native run-android --active-arch-only",
"android:test": "npx react-native run-android --mode=release",
"android:bundle": "npx react-native build-android --mode=release",
"android:apk": "cd android && ./gradlew assembleRelease",
"ios": "react-native run-ios", "ios": "react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "npx react-native start",
"test": "jest" "test": "jest",
"test:i18n": "node __tests__/missingTranslations.js && node __tests__/incorrectTranslationPlaceholders.js",
"adb": "adb devices && adb reverse tcp:9090 tcp:9090 && adb reverse tcp:3000 tcp:3000 && adb reverse tcp:9001 tcp:9001 && adb reverse tcp:8081 tcp:8081",
"adb:unreverse": "adb devices && adb reverse --remove-all",
"adb:inspect": "adb shell input keyevent 82",
"postinstall": "patch-package --exclude 'nothing'"
}, },
"dependencies": { "dependencies": {
"@cashu/cashu-ts": "minibits-cash/cashu-ts#commit=1be675cbaf286df51717f54b402e4fdd6295c506",
"@exodus/borc": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@gandlaf21/bolt11-decode": "^3.1.1",
"@gocodingnow/rn-size-matters": "^0.0.4",
"@notifee/react-native": "^9.0.2",
"@react-native-clipboard/clipboard": "^1.14.1",
"@react-native-community/netinfo": "^11.4.0",
"@react-native-firebase/app": "^20.5.0",
"@react-native-firebase/messaging": "^20.5.0",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@react-navigation/stack": "^6.4.1",
"@sentry/react-native": "^5.32.0",
"date-fns": "^4.0.0",
"i18n-js": "^4.4.3",
"js-lnurl": "^0.6.0",
"lodash.clonedeep": "^4.5.0",
"mobx": "^6.13.2",
"mobx-react-lite": "^4.0.7",
"mobx-state-tree": "^6.0.1",
"nostr-tools": "1.17.0",
"numbro": "^2.5.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"react": "18.3.1", "react": "18.3.1",
"react-native": "0.75.3" "react-native": "0.75.3",
"react-native-animated-pagination-dots": "^0.1.73",
"react-native-camera-kit": "github:teslamotors/react-native-camera-kit#v14.0.0-beta15",
"react-native-code-push": "^9.0.0",
"react-native-dotenv": "^3.4.11",
"react-native-exit-app": "^2.0.0",
"react-native-flash-message": "^0.4.2",
"react-native-gesture-handler": "^2.19.0",
"react-native-json-tree": "^1.3.0",
"react-native-keychain": "^8.2.0",
"react-native-logs": "^5.1.0",
"react-native-mmkv": "^2.12.2",
"react-native-modal": "^13.0.1",
"react-native-pager-view": "^6.4.1",
"react-native-qrcode-svg": "^6.3.2",
"react-native-quick-base64": "^2.1.2",
"react-native-quick-crypto": "^0.7.4",
"react-native-quick-sqlite": "github:margelo/react-native-quick-sqlite#commit=99f34ebefa91698945f3ed26622e002bd79489e0",
"react-native-reanimated": "^3.15.2",
"react-native-safe-area-context": "^4.11.0",
"react-native-screens": "^3.34.0",
"react-native-svg": "^15.6.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"simple-js-task-queue": "^0.2.2",
"text-encoding-polyfill": "^0.6.7",
"util": "^0.12.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",
@@ -21,16 +86,22 @@
"@react-native/eslint-config": "0.75.3", "@react-native/eslint-config": "0.75.3",
"@react-native/metro-config": "0.75.3", "@react-native/metro-config": "0.75.3",
"@react-native/typescript-config": "0.75.3", "@react-native/typescript-config": "0.75.3",
"@tsconfig/react-native": "^3.0.0",
"@types/lodash.clonedeep": "^4.5.9",
"@types/react": "^18.2.6", "@types/react": "^18.2.6",
"@types/react-test-renderer": "^18.0.0", "@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.6.3", "babel-jest": "^29.6.3",
"eslint": "^8.19.0", "babel-plugin-module-resolver": "^5.0.0",
"eslint": "^8.56.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^29.6.3", "jest": "^29.6.3",
"metro-react-native-babel-preset": "0.76.7",
"prettier": "2.8.8", "prettier": "2.8.8",
"react-test-renderer": "18.3.1", "react-test-renderer": "18.3.1",
"typescript": "5.0.4" "typescript": "5.0.4"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18 || 20 || 22"
} },
} "packageManager": "yarn@4.4.1"
}

View File

@@ -0,0 +1,57 @@
diff --git a/node_modules/@scure/bip32/index.ts b/node_modules/@scure/bip32/index.ts
index 51a702f..ac252b2 100644
--- a/node_modules/@scure/bip32/index.ts
+++ b/node_modules/@scure/bip32/index.ts
@@ -8,6 +8,7 @@ import { bytesToHex, concatBytes, createView, hexToBytes, utf8ToBytes } from '@n
import { secp256k1 as secp } from '@noble/curves/secp256k1';
import { mod } from '@noble/curves/abstract/modular';
import { createBase58check } from '@scure/base';
+import QuickCrypto from 'react-native-quick-crypto';
const Point = secp.ProjectivePoint;
const base58check = createBase58check(sha256);
@@ -93,7 +94,9 @@ export class HDKey {
`HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)`
);
}
- const I = hmac(sha512, MASTER_SECRET, seed);
+ //const I = hmac(sha512, MASTER_SECRET, seed);
+ console.log('[fromMasterSeed] Using patched hmac');
+ const I = new Uint8Array(QuickCrypto.createHmac('sha512', MASTER_SECRET).update(seed).digest());
return new HDKey({
versions,
chainCode: I.slice(32),
@@ -217,7 +220,11 @@ export class HDKey {
// Normal child: serP(point(kpar)) || ser32(index)
data = concatBytes(this.pubKey, data);
}
- const I = hmac(sha512, this.chainCode, data);
+
+ // const I = hmac(sha512, this.chainCode, data);
+ console.log('[deriveChild] Using patched hmac');
+ const I = new Uint8Array(QuickCrypto.createHmac('sha512', this.chainCode).update(data).digest());
+
const childTweak = bytesToNumber(I.slice(0, 32));
const chainCode = I.slice(32);
if (!secp.utils.isValidPrivateKey(childTweak)) {
diff --git a/node_modules/@scure/bip32/package.json b/node_modules/@scure/bip32/package.json
index 15a1215..1aea96c 100644
--- a/node_modules/@scure/bip32/package.json
+++ b/node_modules/@scure/bip32/package.json
@@ -6,15 +6,8 @@
"index.ts",
"./lib"
],
- "main": "./lib/index.js",
- "module": "./lib/esm/index.js",
+ "main": "./index.ts",
"types": "./lib/index.d.ts",
- "exports": {
- ".": {
- "import": "./lib/esm/index.js",
- "require": "./lib/index.js"
- }
- },
"dependencies": {
"@noble/curves": "~1.6.0",
"@noble/hashes": "~1.5.0",

View File

@@ -0,0 +1,35 @@
diff --git a/node_modules/@scure/bip39/package.json b/node_modules/@scure/bip39/package.json
index a903f6e..0bc8651 100644
--- a/node_modules/@scure/bip39/package.json
+++ b/node_modules/@scure/bip39/package.json
@@ -45,7 +45,7 @@
"fetch-wordlist": "./scripts/fetch-wordlist.js"
},
"sideEffects": false,
- "main": "index.js",
+ "main": "src/index.ts",
"types": "./index.d.ts",
"exports": {
".": {
diff --git a/node_modules/@scure/bip39/src/index.ts b/node_modules/@scure/bip39/src/index.ts
index e6afe12..c1d593b 100644
--- a/node_modules/@scure/bip39/src/index.ts
+++ b/node_modules/@scure/bip39/src/index.ts
@@ -5,6 +5,7 @@ import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { randomBytes } from '@noble/hashes/utils';
import { utils as baseUtils } from '@scure/base';
+import QuickCrypto from 'react-native-quick-crypto'
// Japanese wordlist
const isJapanese = (wordlist: string[]) => wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093';
@@ -142,5 +143,8 @@ export function mnemonicToSeed(mnemonic: string, passphrase = '') {
* // new Uint8Array([...64 bytes])
*/
export function mnemonicToSeedSync(mnemonic: string, passphrase = '') {
- return pbkdf2(sha512, normalize(mnemonic).nfkd, salt(passphrase), { c: 2048, dkLen: 64 });
+ const original = pbkdf2(sha512, normalize(mnemonic).nfkd, salt(passphrase), { c: 2048, dkLen: 64 });
+ const updated = new Uint8Array(QuickCrypto.pbkdf2Sync(normalize(mnemonic).nfkd, salt(passphrase), 2048, 64, 'sha512'));
+ console.log('[mnemonicToSeedSync] Using patched pbkdf2', {original, updated})
+ return updated
}

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModuleBuilder.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModuleBuilder.java
index b144915..278989b 100644
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModuleBuilder.java
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModuleBuilder.java
@@ -3,7 +3,7 @@ package com.oblador.keychain;
import com.facebook.react.bridge.ReactApplicationContext;
public class KeychainModuleBuilder {
- public static final boolean DEFAULT_USE_WARM_UP = true;
+ public static final boolean DEFAULT_USE_WARM_UP = false;
private ReactApplicationContext reactContext;
private boolean useWarmUp = DEFAULT_USE_WARM_UP;

7
react-native.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
project: {
automaticPodsInstallation: true,
android:{}
},
assets:['./assets/fonts/', './assets/icons/'],
}

110
src/App.tsx Normal file
View File

@@ -0,0 +1,110 @@
import React, { useEffect } from 'react'
import * as Sentry from '@sentry/react-native'
import {
APP_ENV,
SENTRY_DSN,
JS_BUNDLE_VERSION,
NATIVE_VERSION_ANDROID,
CODEPUSH_STAGING_DEPLOYMENT_KEY,
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
} from '@env'
import codePush from 'react-native-code-push'
import messaging from '@react-native-firebase/messaging'
import FlashMessage from "react-native-flash-message"
import {
initialWindowMetrics,
SafeAreaProvider,
} from 'react-native-safe-area-context'
import {
setSizeMattersBaseHeight,
setSizeMattersBaseWidth
} from '@gocodingnow/rn-size-matters'
import {AppNavigator} from './navigation'
import {useInitialRootStore, useStores} from './models'
import {Database} from './services'
import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary'
import Config from './config'
import {log} from './services'
import {Env} from './utils/envtypes'
import AppError from './utils/AppError'
// RN 0.73 screen rendering issue
//import { enableFreeze, enableScreens } from 'react-native-screens';
// enableScreens(false)
setSizeMattersBaseWidth(375)
setSizeMattersBaseHeight(812)
if (!__DEV__) {
Sentry.init({
dsn: SENTRY_DSN,
environment: APP_ENV,
release: `minibits_wallet_android@${JS_BUNDLE_VERSION}`,
dist: NATIVE_VERSION_ANDROID,
beforeSend: function (event, hint) {
const exception = hint.originalException
if (exception instanceof AppError) {
event.fingerprint = [exception.name.toString()]
}
return event
},
enableTracing: false
})
}
interface AppProps {
appName: string
}
function App(props: AppProps) {
const {userSettingsStore, relaysStore, walletProfileStore} = useStores()
const {rehydrated} = useInitialRootStore(async() => {
log.trace('[useInitialRootStore]', 'Root store rehydrated')
// This runs after the root store has been initialized and rehydrated from storage.
// Creates and opens a sqlite database that stores transactions history and user settings.
// It triggers db migrations if database version has changed.
Database.getDatabaseVersion()
// Syncs userSettings store with the database (needed?)
userSettingsStore.loadUserSettings()
// FCM push notifications - set or refresh device token on app start
await messaging().registerDeviceForRemoteMessages()
const deviceToken = await messaging().getToken()
log.debug('[useInitialRootStore]', {deviceToken})
// Make sure profile has already been created (i.e. this is not first run)
if(walletProfileStore.pubkey && deviceToken) {
// if device token changed, update the server
if(deviceToken !== walletProfileStore.device) {
await walletProfileStore.setDevice(deviceToken)
}
}
// Set initial websocket to close as it might have remained open on last app close
relaysStore.resetStatuses()
log.trace('[useInitialRootStore]', 'App is ready to render')
})
if (!rehydrated) {
return null
}
return (
<SafeAreaProvider>
<ErrorBoundary catchErrors={Config.catchErrors}>
<AppNavigator />
<FlashMessage position='bottom' />
</ErrorBoundary>
</SafeAreaProvider>
)
}
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
const codePushOptions = { deploymentKey, checkFrequency: codePush.CheckFrequency.MANUAL }
export default codePush(codePushOptions)(App)

View File

@@ -0,0 +1,72 @@
import React, { useLayoutEffect, useState } from "react"
import { Image, ImageProps, ImageURISource, Platform } from "react-native"
// TODO: document new props
export interface AutoImageProps extends ImageProps {
/**
* How wide should the image be?
*/
maxWidth?: number
/**
* How tall should the image be?
*/
maxHeight?: number
}
/**
* A hook that will return the scaled dimensions of an image based on the
* provided dimensions' aspect ratio. If no desired dimensions are provided,
* it will return the original dimensions of the remote image.
*
* How is this different from `resizeMode: 'contain'`? Firstly, you can
* specify only one side's size (not both). Secondly, the image will scale to fit
* the desired dimensions instead of just being contained within its image-container.
*
*/
export function useAutoImage(
remoteUri: string,
dimensions?: [maxWidth: number, maxHeight: number],
): [width: number, height: number] {
const [[remoteWidth, remoteHeight], setRemoteImageDimensions] = useState([0, 0])
const remoteAspectRatio = remoteWidth / remoteHeight
const [maxWidth, maxHeight] = dimensions ?? []
useLayoutEffect(() => {
if (!remoteUri) return
Image.getSize(remoteUri, (w, h) => setRemoteImageDimensions([w, h]))
}, [remoteUri])
if (Number.isNaN(remoteAspectRatio)) return [0, 0]
if (maxWidth && maxHeight) {
const aspectRatio = Math.min(maxWidth / remoteWidth, maxHeight / remoteHeight)
return [remoteWidth * aspectRatio, remoteHeight * aspectRatio]
} else if (maxWidth) {
return [maxWidth, maxWidth / remoteAspectRatio]
} else if (maxHeight) {
return [maxHeight * remoteAspectRatio, maxHeight]
} else {
return [remoteWidth, remoteHeight]
}
}
/**
* An Image component that automatically sizes a remote or data-uri image.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-AutoImage.md)
*/
export function AutoImage(props: AutoImageProps) {
const { maxWidth, maxHeight, ...ImageProps } = props
const source = props.source as ImageURISource
const [width, height] = useAutoImage(
Platform.select({
web: (source?.uri as string) ?? (source as string),
default: source?.uri as string,
}),
[maxWidth, maxHeight],
)
return <Image {...ImageProps} style={[{ width, height }, props.style]} />
}

View File

@@ -0,0 +1,83 @@
import { observer } from "mobx-react-lite"
import React from "react"
import { ColorValue, Image, TextStyle, View, ViewStyle } from "react-native"
import { Icon, Text } from "."
import { spacing, useThemeColor } from "../theme"
import { getImageSource } from '../utils/utils'
import { iconRegistry } from '.'
export interface AvatarHeaderProps {
heading?: string,
text?: string,
picture?: string
/** default is 96 for pic, 48 for icon - another option is 90 or 80 */
pictureHeight?: number,
/** @default 0.2 */
headerHeightModifier?: number,
fallbackIcon?: keyof typeof iconRegistry
fallbackIconComponent?: React.ReactNode
headerBgColor?: string
encircle?: boolean
children?: React.ReactNode
}
export const AvatarHeader = observer(function (props: AvatarHeaderProps) {
const headerBg = useThemeColor('header')
const borderColor = useThemeColor('border')
return (
<View style={[$headerContainer(props.headerHeightModifier ?? 0.2), { backgroundColor: props.headerBgColor || headerBg }]}>
<View style={{ marginBottom: spacing.small }}>
{props.picture ? (
<Image
style={{
width: 90,
height: props.pictureHeight ?? 96,
borderRadius: 100,
}}
source={{ uri: getImageSource(props.picture) }}
/>
) : (
<>
{props.fallbackIconComponent && (
<View style={props?.encircle ? $encircledIcon(borderColor, props?.pictureHeight) : {}}>
{props.fallbackIconComponent}
</View>
)}
{props.fallbackIcon && !props.fallbackIconComponent && (
<View style={props?.encircle ? $encircledIcon(borderColor, props?.pictureHeight) : {}}>
<Icon
icon={props?.fallbackIcon ?? 'faCircleUser'}
size={props?.encircle ? 35 : (props?.pictureHeight ?? 80)}
color='white'
/>
</View>
)}
</>
)}
</View>
{props.heading && <Text style={{ fontSize: 26, lineHeight: 40 }} text={props.heading} adjustsFontSizeToFit={true} numberOfLines={1} />}
{props.text && <Text preset='bold' text={props.text} style={{ color: 'white', marginBottom: spacing.small }} numberOfLines={2} />}
{props.children}
</View>
)
})
const $headerContainer = (heightModifier: number) => ({
alignItems: 'center',
paddingHorizontal: spacing.medium,
height: spacing.screenHeight * heightModifier,
} satisfies TextStyle)
const $encircledIcon = (borderColor: ColorValue, size: number = 90) => ({
alignItems: 'center',
justifyContent: 'center',
padding: spacing.small,
borderWidth: 2,
borderColor: borderColor,
borderRadius: 100,
width: size,
height: size
} as ViewStyle)

View File

@@ -0,0 +1,234 @@
import React, { ComponentType, Fragment, ReactElement } from "react"
import Modal from 'react-native-modal'
import {
StyleProp,
TextStyle,
ViewProps,
View,
ViewStyle,
ColorValue,
StatusBar,
} from "react-native"
import { colors, useThemeColor, spacing } from "../theme"
import { Text, TextProps } from "./Text"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { Header } from "@react-navigation/stack"
interface ModalProps extends ViewProps {
isVisible?: boolean
top?: number
onBackdropPress?: any
onBackButtonPress?: any
backdropOpacity?: number
/**
* The heading text to display if not using `headingTx`.
*/
heading?: TextProps["text"]
/**
* Heading text which is looked up via i18n.
*/
headingTx?: TextProps["tx"]
/**
* Optional heading options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
headingTxOptions?: TextProps["txOptions"]
/**
* Style overrides for heading text.
*/
headingStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the heading Text component.
*/
HeadingTextProps?: TextProps
/**
* Custom heading component.
* Overrides all other `heading*` props.
*/
HeadingComponent?: ReactElement
/**
* The content text to display if not using `contentTx`.
*/
content?: TextProps["text"]
/**
* Content text which is looked up via i18n.
*/
contentTx?: TextProps["tx"]
/**
* Optional content options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
contentTxOptions?: TextProps["txOptions"]
/**
* Style overrides for content text.
*/
contentStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the content Text component.
*/
ContentTextProps?: TextProps
/**
* Custom content component.
* Overrides all other `content*` props.
*/
ContentComponent?: ReactElement
/**
* The footer text to display if not using `footerTx`.
*/
footer?: TextProps["text"]
/**
* Footer text which is looked up via i18n.
*/
footerTx?: TextProps["tx"]
/**
* Optional footer options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
footerTxOptions?: TextProps["txOptions"]
/**
* Style overrides for footer text.
*/
footerStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the footer Text component.
*/
FooterTextProps?: TextProps
/**
* Custom footer component.
* Overrides all other `footer*` props.
*/
FooterComponent?: ReactElement
}
/**
* Modal
*/
export function BottomModal(props: ModalProps) {
const {
isVisible = true,
onBackdropPress,
onBackButtonPress,
backdropOpacity = 0.4, // if changed, statusBarOnModalOpen theme needs to be adjusted
content,
contentTx,
contentTxOptions,
footer,
footerTx,
footerTxOptions,
heading,
headingTx,
headingTxOptions,
ContentComponent,
HeadingComponent,
FooterComponent,
style: $containerStyleOverride,
contentStyle: $contentStyleOverride,
headingStyle: $headingStyleOverride,
footerStyle: $footerStyleOverride,
ContentTextProps,
HeadingTextProps,
FooterTextProps,
...otherProps
} = props
const insets = useSafeAreaInsets()
const $innerContainerStyle = [
$innerContainerBase, { backgroundColor: useThemeColor('card'), paddingBottom: insets.bottom + 60 }, $containerStyleOverride
]
const isHeadingPresent = !!(HeadingComponent || heading || headingTx)
const isContentPresent = !!(ContentComponent || content || contentTx)
const isFooterPresent = !!(FooterComponent || footer || footerTx)
const $headingStyle = [
(isFooterPresent || isContentPresent) && { marginBottom: spacing.micro },
$headingStyleOverride,
HeadingTextProps?.style,
]
const $contentStyle = [
isHeadingPresent && { marginTop: spacing.micro },
isFooterPresent && { marginBottom: spacing.micro },
$contentStyleOverride,
ContentTextProps?.style,
]
const $footerStyle = [
(isHeadingPresent || isContentPresent) && { marginTop: spacing.micro },
$footerStyleOverride,
FooterTextProps?.style,
]
const statusBarOnModalOpen = useThemeColor('statusBarOnModalOpen')
return (
<Modal
isVisible={isVisible}
statusBarTranslucent={false}
avoidKeyboard={true}
onBackdropPress={onBackdropPress}
onBackButtonPress={onBackButtonPress}
backdropOpacity={backdropOpacity}
style={[$outerContainerBase]}
{...otherProps}
>
<StatusBar backgroundColor={isVisible ? statusBarOnModalOpen : undefined} />
<View style={[$innerContainerBase, $innerContainerStyle]}>
{HeadingComponent ||
(isHeadingPresent && (
<Text
weight="bold"
text={heading}
tx={headingTx}
txOptions={headingTxOptions}
{...HeadingTextProps}
style={$headingStyle}
/>
))}
{ContentComponent ||
(isContentPresent && (
<Text
weight="normal"
text={content}
tx={contentTx}
txOptions={contentTxOptions}
{...ContentTextProps}
style={$contentStyle}
/>
))}
{FooterComponent ||
(isFooterPresent && (
<Text
weight="normal"
size="xs"
text={footer}
tx={footerTx}
txOptions={footerTxOptions}
{...FooterTextProps}
style={$footerStyle}
/>
))}
</View>
</Modal>
)
}
const $outerContainerBase: ViewStyle = {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center',
width: '100%',
margin: 0
}
const $innerContainerBase: ViewStyle = {
width: '100%',
alignItems: 'center',
borderTopLeftRadius: spacing.small,
borderTopRightRadius: spacing.small,
padding: spacing.small
}

View File

@@ -0,0 +1 @@
export const BtcIcon = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="256px" height="256px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M63.04 39.741c-4.274 17.143-21.638 27.575-38.783 23.301C7.12 58.768-3.313 41.404.962 24.262 5.234 7.117 22.597-3.317 39.737.957c17.144 4.274 27.576 21.64 23.302 38.784z" fill="#f7931a"/><path d="M46.11 27.441c.636-4.258-2.606-6.547-7.039-8.074l1.438-5.768-3.512-.875-1.4 5.616c-.922-.23-1.87-.447-2.812-.662l1.41-5.653-3.509-.875-1.439 5.766c-.764-.174-1.514-.346-2.242-.527l.004-.018-4.842-1.209-.934 3.75s2.605.597 2.55.634c1.422.355 1.68 1.296 1.636 2.042l-1.638 6.571c.098.025.225.061.365.117l-.37-.092-2.297 9.205c-.174.432-.615 1.08-1.609.834.035.051-2.552-.637-2.552-.637l-1.743 4.02 4.57 1.139c.85.213 1.683.436 2.502.646l-1.453 5.835 3.507.875 1.44-5.772c.957.26 1.887.5 2.797.726L27.504 50.8l3.511.875 1.453-5.823c5.987 1.133 10.49.676 12.383-4.738 1.527-4.36-.075-6.875-3.225-8.516 2.294-.531 4.022-2.04 4.483-5.157zM38.087 38.69c-1.086 4.36-8.426 2.004-10.807 1.412l1.928-7.729c2.38.594 10.011 1.77 8.88 6.317zm1.085-11.312c-.99 3.966-7.1 1.951-9.083 1.457l1.748-7.01c1.983.494 8.367 1.416 7.335 5.553z" fill="#ffffff"/></svg>`

197
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,197 @@
import React, { ComponentType } from "react"
import {
Pressable,
PressableProps,
PressableStateCallbackType,
StyleProp,
TextStyle,
ViewStyle,
} from "react-native"
import {moderateScale, moderateVerticalScale, scale, verticalScale} from '@gocodingnow/rn-size-matters'
import { colors, spacing, typography, useThemeColor } from "../theme"
import { Text, TextProps } from "./Text"
type Presets = keyof typeof $viewPresets
export interface ButtonAccessoryProps {
style: StyleProp<any>
pressableState: PressableStateCallbackType
}
export interface ButtonProps extends PressableProps {
/**
* Text which is looked up via i18n.
*/
tx?: TextProps["tx"]
/**
* The text to display if not using `tx` or nested components.
*/
text?: TextProps["text"]
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
txOptions?: TextProps["txOptions"]
/**
* An optional style override useful for padding & margin.
*/
style?: StyleProp<ViewStyle>
/**
* An optional style override for the "pressed" state.
*/
pressedStyle?: StyleProp<ViewStyle>
/**
* An optional style override for the button text.
*/
textStyle?: StyleProp<TextStyle>
/**
* An optional style override for the button text when in the "pressed" state.
*/
pressedTextStyle?: StyleProp<TextStyle>
/**
* One of the different types of button presets.
*/
preset?: Presets
/**
* An optional component to render on the right side of the text.
* Example: `RightAccessory={(props) => <View {...props} />}`
*/
RightAccessory?: ComponentType<ButtonAccessoryProps>
/**
* An optional component to render on the left side of the text.
* Example: `LeftAccessory={(props) => <View {...props} />}`
*/
LeftAccessory?: ComponentType<ButtonAccessoryProps>
/**
* Children components.
*/
children?: React.ReactNode
}
/**
* A component that allows users to take actions and make choices.
* Wraps the Text component with a Pressable component.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Button.md)
*/
export function Button(props: ButtonProps) {
const {
tx,
text,
txOptions,
preset = "default",
style: $viewStyleOverride,
pressedStyle: $pressedViewStyleOverride,
textStyle: $textStyleOverride,
pressedTextStyle: $pressedTextStyleOverride,
children,
RightAccessory,
LeftAccessory,
...rest
} = props
const textColor = useThemeColor("text")
const defaultBg = useThemeColor("button")
const defaultBgPressed = useThemeColor("buttonPressed")
const secondaryBg = useThemeColor("buttonSecondary")
const secondaryBgPressed = useThemeColor("buttonSecondaryPressed")
const tertiaryBg = useThemeColor("buttonTertiary")
const tertiaryBgPressed = useThemeColor("buttonTertiaryPressed")
const $pressedViewPresets: Record<Presets, StyleProp<ViewStyle>> = {
default: { backgroundColor: defaultBgPressed },
secondary: { backgroundColor: secondaryBgPressed },
tertiary: { backgroundColor: tertiaryBgPressed },
}
function $viewStyle({ pressed }) {
return [
$viewPresets[preset],
{ backgroundColor: defaultBg },
preset === "secondary" && { backgroundColor: secondaryBg },
preset === "tertiary" && { backgroundColor: tertiaryBg },
!!LeftAccessory && (text || tx) && {paddingLeft: spacing.tiny},
$viewStyleOverride,
!!pressed && [$pressedViewPresets[preset], $pressedViewStyleOverride],
]
}
function $textStyle({ pressed }) {
return [
$textPresets[preset],
{ color: textColor },
preset === "default" && { color: 'white' },
$textStyleOverride,
!!pressed && [$pressedTextPresets[preset], $pressedTextStyleOverride],
]
}
return (
<Pressable style={$viewStyle} accessibilityRole="button" {...rest}>
{(state) => (
<>
{!!LeftAccessory && <LeftAccessory style={$leftAccessoryStyle} pressableState={state} />}
<Text tx={tx} text={text} txOptions={txOptions} style={$textStyle(state)}>
{children}
</Text>
{!!RightAccessory && (
<RightAccessory style={$rightAccessoryStyle} pressableState={state} />
)}
</>
)}
</Pressable>
)
}
const $baseViewStyle: ViewStyle = {
minHeight: moderateVerticalScale(50),
borderRadius: spacing.extraSmall,
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
paddingVertical: spacing.small,
paddingLeft: spacing.small,
paddingRight: spacing.small,
overflow: "hidden",
}
const $baseTextStyle: TextStyle = {
fontSize: moderateVerticalScale(16),
lineHeight: moderateVerticalScale(20),
fontFamily: typography.primary?.light,
textAlign: "center",
flexShrink: 1,
flexGrow: 0,
zIndex: 2,
}
const $rightAccessoryStyle: ViewStyle = { zIndex: 1 }
const $leftAccessoryStyle: ViewStyle = { zIndex: 1 }
const $viewPresets = {
default: [
$baseViewStyle,
] as StyleProp<ViewStyle>,
secondary: [
$baseViewStyle,
] as StyleProp<ViewStyle>,
tertiary: [
$baseViewStyle,
] as StyleProp<ViewStyle>,
}
const $textPresets: Record<Presets, StyleProp<TextStyle>> = {
default: $baseTextStyle,
secondary: $baseTextStyle,
tertiary: $baseTextStyle,
}
const $pressedTextPresets: Record<Presets, StyleProp<TextStyle>> = {
default: { opacity: 0.9 },
secondary: { opacity: 0.9 },
tertiary: { opacity: 0.9 },
}

368
src/components/Card.tsx Normal file
View File

@@ -0,0 +1,368 @@
import { verticalScale } from "@gocodingnow/rn-size-matters"
import React, { ComponentType, Fragment, ReactElement } from "react"
import {
StyleProp,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
useColorScheme
} from "react-native"
import { colors, useThemeColor, spacing } from "../theme"
import { $sizeStyles, Text, TextProps } from "./Text"
type Presets = keyof typeof $containerPresets
interface CardProps extends TouchableOpacityProps {
/**
* One of the different types of text presets.
*/
preset?: Presets
/**
* How the content should be aligned vertically. This is especially (but not exclusively) useful
* when the card is a fixed height but the content is dynamic.
*
* `top` (default) - aligns all content to the top.
* `center` - aligns all content to the center.
* `space-between` - spreads out the content evenly.
* `force-footer-bottom` - aligns all content to the top, but forces the footer to the bottom.
*/
verticalAlignment?: "top" | "center" | "space-between" | "force-footer-bottom"
/**
* Custom component added to the left of the card body.
*/
LeftComponent?: ReactElement
/**
* Custom component added to the right of the card body.
*/
RightComponent?: ReactElement
/**
* The label text to display if not using `headingTx`.
*/
label?: TextProps["text"]
/**
* Label text which is looked up via i18n.
*/
labelTx?: TextProps["tx"]
/**
* Optional label options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
labelTxOptions?: TextProps["txOptions"]
/**
* Style overrides for label text.
*/
labelStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the label Text component.
*/
LabelTextProps?: TextProps
/**
* Custom label component.
* Overrides all other `heading*` props.
*/
LabelComponent?: ReactElement
/**
* Heading text.
*/
heading?: TextProps["text"]
/**
* Heading text which is looked up via i18n.
*/
headingTx?: TextProps["tx"]
/**
* Optional heading options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
headingTxOptions?: TextProps["txOptions"]
/**
* Style overrides for heading text.
*/
headingStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the heading Text component.
*/
HeadingTextProps?: TextProps
/**
* Custom heading component.
* Overrides all other `heading*` props.
*/
HeadingComponent?: ReactElement
/**
* The content text to display if not using `contentTx`.
*/
content?: TextProps["text"]
/**
* Content text which is looked up via i18n.
*/
contentTx?: TextProps["tx"]
/**
* Optional content options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
contentTxOptions?: TextProps["txOptions"]
/**
* Style overrides for content text.
*/
contentStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the content Text component.
*/
ContentTextProps?: TextProps
/**
* Custom content component.
* Overrides all other `content*` props.
*/
ContentComponent?: ReactElement
/**
* The footer text to display if not using `footerTx`.
*/
footer?: TextProps["text"]
/**
* Footer text which is looked up via i18n.
*/
footerTx?: TextProps["tx"]
/**
* Optional footer options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
footerTxOptions?: TextProps["txOptions"]
/**
* Style overrides for footer text.
*/
footerStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the footer Text component.
*/
FooterTextProps?: TextProps
/**
* Custom footer component.
* Overrides all other `footer*` props.
*/
FooterComponent?: ReactElement
}
/**
* Cards are useful for displaying related information in a contained way.
* If a ListItem displays content horizontally, a Card can be used to display content vertically.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Card.md)
*/
export const Card = function (props: CardProps) {
const backgroundColor = useThemeColor('card')
const labelColor = useThemeColor('textDim')
const {
preset = "default",
content,
contentTx,
contentTxOptions,
footer,
footerTx,
footerTxOptions,
label,
labelTx,
labelTxOptions,
heading,
headingTx,
headingTxOptions,
LabelComponent,
ContentComponent,
HeadingComponent,
FooterComponent,
LeftComponent,
RightComponent,
verticalAlignment = "top",
style: $containerStyleOverride,
contentStyle: $contentStyleOverride,
headingStyle: $headingStyleOverride,
footerStyle: $footerStyleOverride,
labelStyle: $labelStyleOverride,
ContentTextProps,
LabelTextProps,
HeadingTextProps,
FooterTextProps,
...WrapperProps
} = props
const isPressable = !!WrapperProps.onPress
const isLabelPresent = !!(LabelComponent || label || labelTx)
const isHeadingPresent = !!(HeadingComponent || heading || headingTx)
const isContentPresent = !!(ContentComponent || content || contentTx)
const isFooterPresent = !!(FooterComponent || footer || footerTx)
const OuterWrapper = isLabelPresent ? View : Fragment
const Wrapper: ComponentType<TouchableOpacityProps> = isPressable ? TouchableOpacity : View
const HeaderContentWrapper = verticalAlignment === "force-footer-bottom" ? View : Fragment
const $containerStyle = [$containerPresets[preset], { backgroundColor }, $containerStyleOverride]
const $labelStyle = [
$labelPresets[preset],
$labelStyleOverride,
LabelTextProps?.style,
{
color: labelColor,
marginHorizontal: spacing.extraSmall,
marginVertical: spacing.tiny,
}
]
const $headingStyle = [
$headingPresets[preset],
(isFooterPresent || isContentPresent) && { marginBottom: spacing.micro },
$headingStyleOverride,
HeadingTextProps?.style,
]
const $contentStyle = [
$contentPresets[preset],
isHeadingPresent && { marginTop: spacing.small },
isFooterPresent && { marginBottom: spacing.small },
$contentStyleOverride,
ContentTextProps?.style,
]
const $footerStyle = [
$footerPresets[preset],
(isHeadingPresent || isContentPresent) && { marginTop: spacing.micro },
$footerStyleOverride,
FooterTextProps?.style,
]
const $alignmentWrapperStyle = [
$alignmentWrapper,
{ justifyContent: $alignmentWrapperFlexOptions[verticalAlignment] },
LeftComponent && { marginStart: spacing.medium },
RightComponent && { marginEnd: spacing.medium },
]
return (
<OuterWrapper>
{LabelComponent ||
(isLabelPresent && (
<Text
size='xs'
preset='formHelper'
text={label}
tx={labelTx}
txOptions={labelTxOptions}
{...LabelTextProps}
style={$labelStyle}
/>
))}
<Wrapper
style={$containerStyle}
activeOpacity={0.8}
accessibilityRole={isPressable ? "button" : undefined}
{...WrapperProps}
>
{LeftComponent}
<View style={$alignmentWrapperStyle}>
<HeaderContentWrapper>
{HeadingComponent ||
(isHeadingPresent && (
<Text
//weight="bold"
text={heading}
tx={headingTx}
txOptions={headingTxOptions}
{...HeadingTextProps}
style={$headingStyle}
/>
))}
{ContentComponent ||
(isContentPresent && (
<Text
//weight="normal"
text={content}
tx={contentTx}
txOptions={contentTxOptions}
{...ContentTextProps}
style={$contentStyle}
/>
))}
</HeaderContentWrapper>
{FooterComponent ||
(isFooterPresent && (
<Text
//weight="normal"
size="xs"
text={footer}
tx={footerTx}
txOptions={footerTxOptions}
{...FooterTextProps}
style={$footerStyle}
/>
))}
</View>
{RightComponent}
</Wrapper>
</OuterWrapper>
)
}
const $containerBase: ViewStyle = {
borderRadius: spacing.medium,
paddingHorizontal: spacing.medium,
paddingVertical: spacing.small,
// borderWidth: 1,
shadowColor: colors.palette.neutral600,
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 5,
minHeight: verticalScale(64),
flexDirection: "row",
}
const $alignmentWrapper: ViewStyle = {
flex: 1,
// alignSelf: "stretch",
alignSelf: 'center'
}
const $alignmentWrapperFlexOptions = {
top: "flex-start",
center: "center",
"space-between": "space-between",
"force-footer-bottom": "space-between",
} as const
const $containerPresets = {
default: [
$containerBase,
{
backgroundColor: colors.palette.neutral100,
borderColor: colors.palette.neutral300,
},
] as StyleProp<ViewStyle>,
reversed: [
$containerBase,
{ backgroundColor: colors.palette.neutral800, borderColor: colors.palette.neutral500 },
] as StyleProp<ViewStyle>,
}
const $labelPresets: Record<Presets, TextStyle> = {
default: {},
reversed: { color: colors.palette.neutral100 },
}
const $headingPresets: Record<Presets, TextStyle> = {
default: {},
reversed: { color: colors.palette.neutral100 },
}
const $contentPresets: Record<Presets, TextStyle> = {
default: {},
reversed: { color: colors.palette.neutral100 },
}
const $footerPresets: Record<Presets, TextStyle> = {
default: {},
reversed: { color: colors.palette.neutral100 },
}

View File

@@ -0,0 +1,56 @@
import React, { useState } from "react"
import { Pressable, View } from "react-native"
import { Text, TextProps } from "./Text"
import { translate } from "../i18n"
import { useThemeColor } from "../theme"
import { Icon } from "./Icon"
import { LayoutAnimation } from "react-native"
interface CollapsibleProps {
summary?: string
text: string
collapsed?: boolean
textProps?: TextProps
}
const collapsedLines = 2
const maxLines = 50
export const CollapsibleText = (props: CollapsibleProps) => {
const [collapsed, setCollapsed] = useState(props?.collapsed ?? false)
const toggleCollapse = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setCollapsed(!collapsed)
}
const textDim = useThemeColor('textDim')
let summary = props?.summary
if (!summary) {
if (summary?.includes("\n")) {
summary = summary.split("\n")[0]
} else {
summary = props?.text.slice(0, 100)
}
}
// summary += "\u2026" // add a unicode ellipsis character
return (
<View>
{props.text.trim() === '' ? (
<Text text={summary} {...props.textProps} />
) : (
<Pressable onPress={toggleCollapse}>
<Text
text={collapsed ? summary : props.text}
numberOfLines={collapsed ? collapsedLines : maxLines}
ellipsizeMode="tail"
{...props.textProps}
/>
<View style={{ flexDirection: "row" }}>
<Text tx={collapsed ? 'common.showMore' : 'common.hideMore'} style={{ color: textDim }} size="xs" />
<Icon icon={collapsed ? 'faChevronDown' : 'faChevronUp'} color={textDim} size={12} />
</View>
</Pressable>
)}
</View>
)
}

View File

@@ -0,0 +1,219 @@
import React from "react"
import { Image, ImageProps, ImageStyle, StyleProp, TextStyle, View, ViewStyle } from "react-native"
import { translate } from "../i18n"
import { spacing } from "../theme"
import { Button, ButtonProps } from "./Button"
import { Text, TextProps } from "./Text"
// const sadFace = require("../../assets/images/sad-face.png")
interface EmptyStateProps {
/**
* An optional prop that specifies the text/image set to use for the empty state.
*/
preset?: keyof typeof EmptyStatePresets
/**
* Style override for the container.
*/
style?: StyleProp<ViewStyle>
/**
* An Image source to be displayed above the heading.
*/
imageSource?: ImageProps["source"]
/**
* Style overrides for image.
*/
imageStyle?: StyleProp<ImageStyle>
/**
* Pass any additional props directly to the Image component.
*/
ImageProps?: Omit<ImageProps, "source">
/**
* The heading text to display if not using `headingTx`.
*/
heading?: TextProps["text"]
/**
* Heading text which is looked up via i18n.
*/
headingTx?: TextProps["tx"]
/**
* Optional heading options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
headingTxOptions?: TextProps["txOptions"]
/**
* Style overrides for heading text.
*/
headingStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the heading Text component.
*/
HeadingTextProps?: TextProps
/**
* The content text to display if not using `contentTx`.
*/
content?: TextProps["text"]
/**
* Content text which is looked up via i18n.
*/
contentTx?: TextProps["tx"]
/**
* Optional content options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
contentTxOptions?: TextProps["txOptions"]
/**
* Style overrides for content text.
*/
contentStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the content Text component.
*/
ContentTextProps?: TextProps
/**
* The button text to display if not using `buttonTx`.
*/
button?: TextProps["text"]
/**
* Button text which is looked up via i18n.
*/
buttonTx?: TextProps["tx"]
/**
* Optional button options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
buttonTxOptions?: TextProps["txOptions"]
/**
* Style overrides for button.
*/
buttonStyle?: ButtonProps["style"]
/**
* Style overrides for button text.
*/
buttonTextStyle?: ButtonProps["textStyle"]
/**
* Called when the button is pressed.
*/
buttonOnPress?: ButtonProps["onPress"]
/**
* Pass any additional props directly to the Button component.
*/
ButtonProps?: ButtonProps
}
const EmptyStatePresets = {
generic: {
imageSource: null,
heading: translate("emptyStateComponent.generic.heading"),
content: translate("emptyStateComponent.generic.content"),
button: translate("emptyStateComponent.generic.button"),
},
} as const
/**
* A component to use when there is no data to display. It can be utilized to direct the user what to do next.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-EmptyState.md)
*/
export function EmptyState(props: EmptyStateProps) {
const preset = EmptyStatePresets[props.preset] ? EmptyStatePresets[props.preset] : undefined
const {
button = preset?.button,
buttonTx,
buttonOnPress,
buttonTxOptions,
content = preset?.content,
contentTx,
contentTxOptions,
heading = preset?.heading,
headingTx,
headingTxOptions,
imageSource = preset?.imageSource,
style: $containerStyleOverride,
buttonStyle: $buttonStyleOverride,
buttonTextStyle: $buttonTextStyleOverride,
contentStyle: $contentStyleOverride,
headingStyle: $headingStyleOverride,
imageStyle: $imageStyleOverride,
ButtonProps,
ContentTextProps,
HeadingTextProps,
ImageProps,
} = props
const isImagePresent = !!imageSource
const isHeadingPresent = !!(heading || headingTx)
const isContentPresent = !!(content || contentTx)
const isButtonPresent = !!(button || buttonTx)
const $containerStyles = [$containerStyleOverride]
const $imageStyles = [
$image,
(isHeadingPresent || isContentPresent || isButtonPresent) && { marginBottom: spacing.micro },
$imageStyleOverride,
ImageProps?.style,
]
const $headingStyles = [
$heading,
isImagePresent && { marginTop: spacing.micro },
(isContentPresent || isButtonPresent) && { marginBottom: spacing.micro },
$headingStyleOverride,
HeadingTextProps?.style,
]
const $contentStyles = [
$content,
(isImagePresent || isHeadingPresent) && { marginTop: spacing.micro },
isButtonPresent && { marginBottom: spacing.micro },
$contentStyleOverride,
ContentTextProps?.style,
]
const $buttonStyles = [
(isImagePresent || isHeadingPresent || isContentPresent) && { marginTop: spacing.extraLarge },
$buttonStyleOverride,
ButtonProps?.style,
]
return (
<View style={$containerStyles}>
{isImagePresent && <Image source={imageSource} {...ImageProps} style={$imageStyles} />}
{isHeadingPresent && (
<Text
preset="subheading"
text={heading}
tx={headingTx}
txOptions={headingTxOptions}
{...HeadingTextProps}
style={$headingStyles}
/>
)}
{isContentPresent && (
<Text
text={content}
tx={contentTx}
txOptions={contentTxOptions}
{...ContentTextProps}
style={$contentStyles}
/>
)}
{isButtonPresent && (
<Button
onPress={buttonOnPress}
text={button}
tx={buttonTx}
txOptions={buttonTxOptions}
textStyle={$buttonTextStyleOverride}
{...ButtonProps}
style={$buttonStyles}
/>
)}
</View>
)
}
const $image: ImageStyle = { alignSelf: "center" }
const $heading: TextStyle = { textAlign: "center", paddingHorizontal: spacing.large }
const $content: TextStyle = { textAlign: "center", paddingHorizontal: spacing.large }

View File

@@ -0,0 +1,132 @@
import React, { FC, useState, useEffect } from 'react'
import { View, ScrollView, LayoutAnimation, Platform, UIManager, ViewStyle } from 'react-native'
import { BottomModal, Button, Icon, ListItem, Text } from '../components'
import AppError from '../utils/AppError'
import { spacing, useThemeColor, colors } from '../theme'
import { isObj } from '@cashu/cashu-ts/src/utils'
import JSONTree from 'react-native-json-tree'
import Clipboard from '@react-native-clipboard/clipboard'
if (Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
type ErrorModalProps = {
error: AppError
}
export const ErrorModal: FC<ErrorModalProps> = function ({ error }) {
const [isErrorVisible, setIsErrorVisible] = useState<boolean>(true)
const [isParamsVisible, setIsParamsVisible] = useState<boolean>(false)
// needed for error to re-appear
useEffect(() => {
setIsErrorVisible(true)
}, [error])
const toggleParams = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setIsParamsVisible(previousState => !previousState)
}
const onClose = () => {
setIsErrorVisible(false)
}
const onCopy = function () {
try {
Clipboard.setString(JSON.stringify(error.params))
} catch (e: any) {
return false
}
}
const backgroundColor = useThemeColor('error')
return (
<BottomModal
isVisible={isErrorVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
style={{ backgroundColor }}
ContentComponent={
<>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.small }}>
<Icon icon="faInfoCircle" size={spacing.large} color="white" />
<Text style={{ color: 'white', marginLeft: spacing.small }}>{error.name}</Text>
</View>
<View>
<Text style={{ color: 'white', marginBottom: spacing.small }}>{error.message}</Text>
</View>
{error.params && isObj(error.params) && (
<>
{isParamsVisible ? (
<>
<ScrollView style={{
alignSelf: 'stretch',
borderRadius: spacing.small,
backgroundColor: '#fff',
padding: spacing.tiny,
maxHeight: spacing.screenHeight * 0.3,
maxWidth: spacing.screenWidth * 0.9
}}>
<JSONTree
hideRoot
data={error.params}
theme={{
scheme: 'codeschool',
author: 'brettof86',
base00: '#000000',
base01: '#2e2f30',
base02: '#515253',
base03: '#737475',
base04: '#959697',
base05: '#b7b8b9',
base06: '#dadbdc',
base07: '#fcfdfe',
base08: '#e31a1c',
base09: '#e6550d',
base0A: '#dca060',
base0B: '#31a354',
base0C: '#80b1d3',
base0D: '#3182bd',
base0E: '#756bb1',
base0F: '#b15928'
}}
/>
</ScrollView>
<View style={$buttonContainer}>
<Button
onPress={toggleParams}
text='Hide details'
preset='tertiary'
/>
<Button
onPress={onCopy}
text='Copy'
preset='tertiary'
/>
</View>
</>
) : (
<Button
onPress={toggleParams}
text='Show details'
preset='tertiary'
/>
)}
</>
)}
</>
}
/>
)
}
const $buttonContainer: ViewStyle = {
flexDirection: 'row',
marginTop: spacing.small,
}

View File

@@ -0,0 +1 @@
export const EurIcon = `<svg width="256px" height="256px" viewBox="2 2 20.00 20.00" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(2.16,2.16), scale(0.82)"><rect x="0" y="0" width="24.00" height="24.00" rx="12" fill="#ffffff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.192"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM6.75 12C6.75 11.7454 6.76813 11.495 6.80317 11.25H10C10.4142 11.25 10.75 10.9142 10.75 10.5C10.75 10.0858 10.4142 9.75 10 9.75H7.25522C8.09782 7.97629 9.9057 6.75 12 6.75C12.9575 6.75 13.853 7.00564 14.6245 7.4519C14.983 7.65931 15.4418 7.53678 15.6492 7.17824C15.8566 6.81969 15.7341 6.3609 15.3755 6.15349C14.3819 5.57872 13.2282 5.25 12 5.25C9.06101 5.25 6.56072 7.12832 5.63409 9.75H5C4.58579 9.75 4.25 10.0858 4.25 10.5C4.25 10.9142 4.58579 11.25 5 11.25H5.2912C5.26397 11.4963 5.25 11.7465 5.25 12C5.25 12.2535 5.26397 12.5037 5.2912 12.75H5C4.58579 12.75 4.25 13.0858 4.25 13.5C4.25 13.9142 4.58579 14.25 5 14.25H5.63409C6.56072 16.8717 9.06101 18.75 12 18.75C13.2282 18.75 14.3819 18.4213 15.3755 17.8465C15.7341 17.6391 15.8566 17.1803 15.6492 16.8218C15.4418 16.4632 14.983 16.3407 14.6245 16.5481C13.853 16.9944 12.9575 17.25 12 17.25C9.9057 17.25 8.09782 16.0237 7.25522 14.25H10C10.4142 14.25 10.75 13.9142 10.75 13.5C10.75 13.0858 10.4142 12.75 10 12.75H6.80317C6.76813 12.505 6.75 12.2546 6.75 12Z" fill="#0002C8"></path> </g></svg>`

371
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,371 @@
import React, { ReactElement } from "react"
import {
StyleProp,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
StatusBar,
StatusBarProps,
ColorValue
} from "react-native"
import { useIsFocused } from '@react-navigation/native'
import useIsInternetReachable from '../utils/useIsInternetReachable'
import { translate } from "../i18n"
import { useThemeColor, spacing, colors, typography } from "../theme"
import { displayName as appName } from '../../app.json'
import { ExtendedEdge, useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"
import { Icon, IconTypes } from "./Icon"
import { Text, TextProps } from "./Text"
import { moderateVerticalScale } from "@gocodingnow/rn-size-matters"
export interface HeaderProps {
/**
* The layout of the title relative to the action components.
* - `center` will force the title to always be centered relative to the header. If the title or the action buttons are too long, the title will be cut off.
* - `flex` will attempt to center the title relative to the action buttons. If the action buttons are different widths, the title will be off-center relative to the header.
*/
titleMode?: "center" | "flex"
/**
* Optional title style override.
*/
titleStyle?: StyleProp<TextStyle>
/**
* Optional outer title container style override.
*/
titleContainerStyle?: StyleProp<ViewStyle>
/**
* Optional inner header wrapper style override.
*/
style?: StyleProp<ViewStyle>
/**
* Optional outer header container style override.
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Background color
*/
backgroundColor?: ColorValue
/**
* Title action custom ReactElement.
* Overrides `title`, `titleTx`.
*/
TitleActionComponent?: ReactElement
/**
* Title text to display if not using `tx` or nested components.
*/
title?: TextProps["text"]
/**
* Title text which is looked up via i18n.
*/
titleTx?: TextProps["tx"]
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
titleTxOptions?: TextProps["txOptions"]
/**
* Icon that should appear on the left.
* Can be used with `onLeftPress`.
*/
leftIcon?: IconTypes
/**
* An optional tint color for the left icon
*/
leftIconColor?: string
/**
* Left action text to display if not using `leftTx`.
* Can be used with `onLeftPress`. Overrides `leftIcon`.
*/
leftText?: TextProps["text"]
/**
* Left action text text which is looked up via i18n.
* Can be used with `onLeftPress`. Overrides `leftIcon`.
*/
leftTx?: TextProps["tx"]
/**
* Left action custom ReactElement if the built in action props don't suffice.
* Overrides `leftIcon`, `leftTx` and `leftText`.
*/
LeftActionComponent?: ReactElement
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
leftTxOptions?: TextProps["txOptions"]
/**
* What happens when you press the left icon or text action.
*/
onLeftPress?: TouchableOpacityProps["onPress"]
/**
* Icon that should appear on the right.
* Can be used with `onRightPress`.
*/
rightIcon?: IconTypes
/**
* An optional tint color for the right icon
*/
rightIconColor?: ColorValue
/**
* Right action text to display if not using `rightTx`.
* Can be used with `onRightPress`. Overrides `rightIcon`.
*/
rightText?: TextProps["text"]
/**
* Right action text text which is looked up via i18n.
* Can be used with `onRightPress`. Overrides `rightIcon`.
*/
rightTx?: TextProps["tx"]
/**
* Right action custom ReactElement if the built in action props don't suffice.
* Overrides `rightIcon`, `rightTx` and `rightText`.
*/
RightActionComponent?: ReactElement
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
rightTxOptions?: TextProps["txOptions"]
/**
* What happens when you press the right icon or text action.
*/
onRightPress?: TouchableOpacityProps["onPress"]
/**
* Override the default edges for the safe area.
*/
safeAreaEdges?: ExtendedEdge[]
/**
* Pass any additional props directly to the StatusBar component.
*/
StatusBarProps?: StatusBarProps
}
interface HeaderActionProps {
backgroundColor?: ColorValue
icon?: IconTypes
iconColor?: string
text?: TextProps["text"]
tx?: TextProps["tx"]
txOptions?: TextProps["txOptions"]
onPress?: TouchableOpacityProps["onPress"]
ActionComponent?: ReactElement
}
/* Needed to keep status bar styles on navigation tabs */
function FocusAwareStatusBar(props: StatusBarProps) {
const isFocused = useIsFocused();
return isFocused ? <StatusBar {...props} /> : null;
}
/**
* Header that appears on many screens. Will hold navigation buttons and screen title.
* The Header is meant to be used with the `screenOptions.header` option on navigators, routes, or screen components via `navigation.setOptions({ header })`.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Header.md)
*/
export function Header(props: HeaderProps) {
const isInternetReachable = useIsInternetReachable()
const {
backgroundColor = useThemeColor('header'),
LeftActionComponent,
leftIcon,
leftIconColor = "white",
leftText,
leftTx,
leftTxOptions,
onLeftPress,
onRightPress,
RightActionComponent,
rightIcon,
rightIconColor = "white",
rightText,
rightTx,
rightTxOptions,
safeAreaEdges = ["top"],
TitleActionComponent,
title= appName,
titleMode = "center",
titleTx,
titleTxOptions,
titleContainerStyle: $titleContainerStyleOverride,
style: $styleOverride,
titleStyle: $titleStyleOverride,
containerStyle: $containerStyleOverride,
StatusBarProps,
} = props
const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges)
const titleContent = !!TitleActionComponent ? undefined : titleTx ? translate(titleTx, titleTxOptions) : title
return (
<View style={[$container, $containerInsets, { backgroundColor }, $containerStyleOverride]}>
<FocusAwareStatusBar
backgroundColor={backgroundColor}
// barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
barStyle = 'light-content'
{...StatusBarProps}
/>
<View style={[$wrapper, $styleOverride]}>
<HeaderAction
tx={leftTx}
text={leftText}
icon={leftIcon}
iconColor={leftIconColor as string}
onPress={onLeftPress}
txOptions={leftTxOptions}
backgroundColor={backgroundColor}
ActionComponent={LeftActionComponent}
/>
{!!TitleActionComponent && (
<View
style={[
titleMode === "center" && $titleWrapperCenter,
titleMode === "flex" && $titleWrapperFlex,
$titleContainerStyleOverride,
]}
pointerEvents="none"
>
{TitleActionComponent}
</View>
)}
{!!titleContent && (
<View
style={[
titleMode === "center" && $titleWrapperCenter,
titleMode === "flex" && $titleWrapperFlex,
$titleContainerStyleOverride,
]}
pointerEvents="none"
>
<Text
weight="medium"
size="lg"
text={titleContent}
style={[$title, $titleStyleOverride]}
/>
</View>
)}
<HeaderAction
tx={rightTx}
text={rightText}
icon={rightIcon}
iconColor={rightIconColor as string}
onPress={onRightPress}
txOptions={rightTxOptions}
backgroundColor={backgroundColor}
ActionComponent={RightActionComponent}
/>
</View>
</View>
)
}
function HeaderAction(props: HeaderActionProps) {
const { backgroundColor, icon, text, tx, txOptions, onPress, ActionComponent, iconColor } = props
const content = tx ? translate(tx, txOptions) : text
if (ActionComponent) return ActionComponent
if (content) {
return (
<TouchableOpacity
style={[$actionTextContainer, { backgroundColor }]}
onPress={onPress}
disabled={!onPress}
activeOpacity={0.8}
>
<Text weight="medium" size="md" text={content} style={$actionText} />
</TouchableOpacity>
)
}
if (icon) {
return (
<Icon
size={20}
icon={icon}
color={iconColor}
onPress={onPress}
containerStyle={[$actionIconContainer, { backgroundColor }]}
/>
)
}
return <View style={[$actionFillerContainer, { backgroundColor }]} />
}
const $wrapper: ViewStyle = {
height: 56,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
// overflow: 'visible',
// borderWidth: 1,
// borderColor: 'red',
}
const $container: ViewStyle = {
width: "100%",
}
const $title: TextStyle = {
textAlign: "center",
fontFamily: typography.logo.normal,
color: 'white'
}
const $actionTextContainer: ViewStyle = {
flexGrow: 0,
alignItems: "center",
justifyContent: "center",
height: "100%",
paddingHorizontal: spacing.medium,
zIndex: 2,
}
const $actionText: TextStyle = {
color: colors.light.tint,
}
const $actionIconContainer: ViewStyle = {
flexGrow: 0,
alignItems: "center",
justifyContent: "center",
height: "100%",
paddingHorizontal: spacing.medium,
zIndex: 2,
}
const $actionFillerContainer: ViewStyle = {
width: 16,
}
const $titleWrapperCenter: ViewStyle = {
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
position: "absolute",
paddingHorizontal: spacing.huge,
zIndex: 1,
}
const $titleWrapperFlex: ViewStyle = {
justifyContent: "center",
flexGrow: 1,
}
/* const $offline: TextStyle = {
position: 'absolute',
top: 15,
alignItems: "center",
justifyContent: "center",
fontSize: 10,
fontFamily: typography.code?.normal,
color: colors.palette.accent200,
}*/

124
src/components/HeaderBg.ts Normal file
View File

@@ -0,0 +1,124 @@
export const HeaderBg = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1440" height="200" preserveAspectRatio="none" viewBox="0 0 1440 200">
<g mask="url(&quot;#SvgjsMask1228&quot;)" fill="none">
<rect width="1440" height="200" x="0" y="0" fill="rgba(33, 108, 233, 1)"></rect>
<path d="M747.1057075216023 127.75562642682651L754.3970814083749 75.87480543830213 702.5162604198505 68.58343155152957 695.2248865330779 120.46425254005393z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M327.2048426729167 35.928073236888665L288.5226793723616 68.38626219552378 320.9808683309967 107.06842549607889 359.6630316315518 74.61023653744377z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M692.4246915017623 170.19979034423497L702.9325329152182 215.7142519174232 748.4469944884064 205.20641050396728 737.9391530749505 159.69194893077903z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M1011.3353816446419 62.94159037130783L1028.581667963285 89.49854242930147 1055.1386200212787 72.25225611065815 1037.8923337026356 45.69530405266451z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M969.6723418652923 55.983154930508455L919.0428285294618 52.44279447268238 915.5024680716357 103.07230780851287 966.1319814074662 106.61266826633894z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M490.49547511700763 73.50446578850527L517.250567607621 35.29423377783492 479.04033559695057 8.53914128722161 452.28524310633725 46.749373297891964z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M1421.5534289524064 76.40745393189856L1402.143414506712 126.97227030992588 1452.7082308847394 146.38228475562033 1472.1182453304336 95.817468377593z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M44.18649852640779 56.50055120620612L12.331357360316396 43.63023874742407-0.5389550984656495 75.48537991351547 31.316186067625747 88.35569237229751z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M567.477420162059 5.581617787479144L553.758592711916 33.709382410470965 581.8863573349078 47.42820986061403 595.6051847850508 19.300445237622203z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M1191.7568875108875 151.89571627870086L1189.2314299670188 103.70711568508801 1141.042829373406 106.23257322895671 1143.5682869172747 154.42117382256956z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M1233.0041486652651 43.54078493335189L1218.5612249444973 13.928402945024839 1188.9488429561702 28.371326665792672 1203.391766676938 57.983708654119724z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M1093.9611587853271 54.44612369420207L1125.8106675944764 85.20283683431656 1156.567380734591 53.353328025167215 1124.7178719254416 22.596614885052723z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M1431.181750298965 152.22148179381813L1388.2730376608383 138.27959591769297 1374.3311517847133 181.18830855581956 1417.23986442284 195.13019443194472z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M689.945851702067 69.68986125874905L724.3502661613595 91.18812542474701 745.8485303273575 56.783710965454546 711.444115868065 35.28544679945657z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M592.5530077850946 131.1723799971202L580.8661884602851 167.1407114350277 616.8345198981926 178.8275307598371 628.5213392230021 142.8591993219296z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M1066.3984237341463 93.45412929443842L1015.8249975827404 76.04030217184369 998.4111704601456 126.6137283232496 1048.9845966115515 144.0275554458443z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M520.0887716328363 213.80147367701232L526.5041910090813 161.55207579613258 474.2547931282015 155.13665641988757 467.8393737519565 207.38605430076734z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M1209.4433647244202 151.13012700405633L1208.364068011502 182.03714104259996 1239.2710820500456 183.11643775551815 1240.3503787629638 152.20942371697453z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M1412.5831611707363 137.77032976422595L1425.2780766404287 104.69894429691828 1392.206691173121 92.00402882722595 1379.5117757034286 125.07541429453362z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M200.67441576045212 107.21742873144655L188.93683921376643 134.8694262079934 216.5888366903133 146.6070027546791 228.32641323699897 118.95500527813222z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
</g>
<defs>
<mask id="SvgjsMask1228">
<rect width="1440" height="200" fill="#ffffff"></rect>
</mask>
<style>
@keyframes float1 {
0%{transform: translate(0, 0)}
50%{transform: translate(-10px, 0)}
100%{transform: translate(0, 0)}
}
.triangle-float1 {
animation: float1 5s infinite;
}
@keyframes float2 {
0%{transform: translate(0, 0)}
50%{transform: translate(-5px, -5px)}
100%{transform: translate(0, 0)}
}
.triangle-float2 {
animation: float2 4s infinite;
}
@keyframes float3 {
0%{transform: translate(0, 0)}
50%{transform: translate(0, -10px)}
100%{transform: translate(0, 0)}
}
.triangle-float3 {
animation: float3 6s infinite;
}
</style>
</defs>
</svg>`
export const HeaderBg2 = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1440" height="200" preserveAspectRatio="none" viewBox="0 0 1440 200">
<g mask="url(&quot;#SvgjsMask1105&quot;)" fill="none">
<rect width="1440" height="200" x="0" y="0" fill="rgba(33, 108, 233, 1)"></rect>
<path d="M749.0831241863771 121.38806886260159L754.6023483557941 68.8761586102141 702.0904381034065 63.356934440797104 696.5712139339896 115.86884469318458z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M1351.3054843130171 54.0624429919247L1354.7515332124415 103.3432382069029 1404.0323284274198 99.8971893074785 1400.5862795279954 50.61639409250029z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M94.16973276472656 153.7220346920555L121.01408311325078 123.9083632425251 91.20041166372037 97.06401289400088 64.35606131519616 126.87768434353129z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M466.97021985019103 150.27038846783466L442.0906957238391 124.50688704231337 416.32719429831775 149.38641116866532 441.2067184246697 175.14991259418662z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M589.7063339756562 117.83984879717408L627.6221439178643 106.24782238548573 616.030117506176 68.33201244327765 578.1143075639678 79.924038854966z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M918.9888996274942 100.97281796775246L943.7668434439643 71.44361440737686 914.2376398835886 46.665670590906856 889.4596960671186 76.19487415128246z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M362.0235285710007 60.29738945472211L362.56413622240916 29.325997848238075 331.5927446159251 28.785390196829653 331.0521369645167 59.756781803313686z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M921.2410433137273 123.30434229262224L898.4513471220594 148.61486409224395 923.7618689216811 171.40456028391196 946.551565113349 146.09403848429025z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M305.1087778387738 165.28226340185944L287.71314790346094 133.8997162655259 256.3306007671274 151.29534620083876 273.72623070244026 182.6778933371723z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M1274.054443780785 60.90011888152908L1242.8063434543035 78.221223733202 1260.1274483059765 109.46932405968349 1291.375548632458 92.14821920801057z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M768.4138023884847 183.50966038245656L791.7112207622134 215.57580582570978 823.7773662054667 192.278387451981 800.4799478317378 160.21224200872777z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M955.206998076348 162.76877783195323L999.3710378176617 140.26607563127058 976.8683356169789 96.10203588995692 932.7042958756654 118.60473809063959z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M12.809281352625778 38.32420067506579L15.333160449379674 86.4826827035264 63.49164247784029 83.9588036067725 60.967763381086385 35.800321578311895z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M730.8203243804758 169.9399636519063L759.0370257813572 126.49005377027117 715.587115899722 98.27335236938977 687.3704144988407 141.72326225102492z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M829.0504139631389 113.90023900242355L838.0171519845464 164.75313732450644 888.8700503066293 155.78639930309896 879.9033122852218 104.93350098101605z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
<path d="M1113.9957280681033 78.02099446221344L1140.6850821326325 57.16898575084809 1119.8330734212673 30.47963168631881 1093.1437193567378 51.33164039768416z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M172.06045733524795 59.181689102757716L206.81411742545455 105.30135375782231 252.93378208051914 70.5476936676157 218.18012199031253 24.428029012551107z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M562.8672114116416 114.31024483273946L600.7862711130508 138.93517012974428 625.4111964100557 101.01611042833508 587.4921367086465 76.39118513133025z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
<path d="M888.0288011006094 114.15671294879479L858.1621970497167 92.4573549387241 836.462839039646 122.32395898961676 866.3294430905387 144.02331699968744z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
<path d="M1126.2173189168125 54.03684274720831L1105.9951016004316 26.203348441102374 1078.1616072943254 46.42556575748341 1098.3838246107066 74.25906006358935z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
</g>
<defs>
<mask id="SvgjsMask1105">
<rect width="1440" height="200" fill="#ffffff"></rect>
</mask>
<style>
@keyframes float1 {
0%{transform: translate(0, 0)}
50%{transform: translate(-10px, 0)}
100%{transform: translate(0, 0)}
}
.triangle-float1 {
animation: float1 5s infinite;
}
@keyframes float2 {
0%{transform: translate(0, 0)}
50%{transform: translate(-5px, -5px)}
100%{transform: translate(0, 0)}
}
.triangle-float2 {
animation: float2 4s infinite;
}
@keyframes float3 {
0%{transform: translate(0, 0)}
50%{transform: translate(0, -10px)}
100%{transform: translate(0, 0)}
}
.triangle-float3 {
animation: float3 6s infinite;
}
</style>
</defs>
</svg>`

201
src/components/Icon.tsx Normal file
View File

@@ -0,0 +1,201 @@
import * as React from "react"
import { ComponentType } from "react"
import {
ColorValue,
Image,
ImageStyle,
StyleProp,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from "react-native"
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
import { colors, spacing, useThemeColor } from "../theme"
import { IconDefinition, Transform } from "@fortawesome/fontawesome-svg-core"
// due to Metro bundler not currently supporting tree-shaking, we take the safe routes and use "deep imports" for icons
// metro bundler tree-shaking issue: https://github.com/facebook/metro/issues/132
// relevant font-awesome docs: https://docs.fontawesome.com/apis/javascript/tree-shaking
import { faTwitter } from "@fortawesome/free-brands-svg-icons/faTwitter"
import { faTelegramPlane } from "@fortawesome/free-brands-svg-icons/faTelegramPlane"
import { faDiscord } from "@fortawesome/free-brands-svg-icons/faDiscord"
import { faGithub } from "@fortawesome/free-brands-svg-icons/faGithub"
import { faReddit } from "@fortawesome/free-brands-svg-icons/faReddit"
import { faWallet } from '@fortawesome/free-solid-svg-icons/faWallet'
import { faAddressCard } from '@fortawesome/free-solid-svg-icons/faAddressCard'
import { faAddressBook } from '@fortawesome/free-solid-svg-icons/faAddressBook'
import { faQrcode } from '@fortawesome/free-solid-svg-icons/faQrcode'
import { faClipboard } from '@fortawesome/free-solid-svg-icons/faClipboard'
import { faSliders } from '@fortawesome/free-solid-svg-icons/faSliders'
import { faCoins } from '@fortawesome/free-solid-svg-icons/faCoins'
import { faEllipsisVertical } from '@fortawesome/free-solid-svg-icons/faEllipsisVertical'
import { faEllipsis } from '@fortawesome/free-solid-svg-icons/faEllipsis'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons/faArrowUp'
import { faArrowDown } from '@fortawesome/free-solid-svg-icons/faArrowDown'
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import { faXmark } from '@fortawesome/free-solid-svg-icons/faXmark'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'
import { faBug } from '@fortawesome/free-solid-svg-icons/faBug'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle'
import { faArrowTurnUp } from '@fortawesome/free-solid-svg-icons/faArrowTurnUp'
import { faArrowTurnDown } from '@fortawesome/free-solid-svg-icons/faArrowTurnDown'
import { faPencil } from '@fortawesome/free-solid-svg-icons/faPencil'
import { faTags } from '@fortawesome/free-solid-svg-icons/faTags'
import { faShareFromSquare } from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
import { faRotate } from '@fortawesome/free-solid-svg-icons/faRotate'
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode'
import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'
import { faCircle } from '@fortawesome/free-solid-svg-icons/faCircle'
import { faPaperPlane } from '@fortawesome/free-solid-svg-icons/faPaperPlane'
import { faBolt } from '@fortawesome/free-solid-svg-icons/faBolt'
import { faArrowUpFromBracket } from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
import { faArrowRightToBracket } from '@fortawesome/free-solid-svg-icons/faArrowRightToBracket'
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'
import { faShieldHalved } from '@fortawesome/free-solid-svg-icons/faShieldHalved'
import { faCloudArrowUp } from '@fortawesome/free-solid-svg-icons/faCloudArrowUp'
import { faPaintbrush } from '@fortawesome/free-solid-svg-icons/faPaintbrush'
import { faCopy } from '@fortawesome/free-solid-svg-icons/faCopy'
import { faBurst } from '@fortawesome/free-solid-svg-icons/faBurst'
import { faUserShield } from '@fortawesome/free-solid-svg-icons/faUserShield'
import { faLock } from '@fortawesome/free-solid-svg-icons/faLock'
import { faLockOpen } from '@fortawesome/free-solid-svg-icons/faLockOpen'
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons/faTriangleExclamation'
import { faDownload } from '@fortawesome/free-solid-svg-icons/faDownload'
import { faUpload } from '@fortawesome/free-solid-svg-icons/faUpload'
import { faRecycle } from '@fortawesome/free-solid-svg-icons/faRecycle'
import { faListUl } from '@fortawesome/free-solid-svg-icons/faListUl'
import { faExpand } from '@fortawesome/free-solid-svg-icons/faExpand'
import { faFingerprint } from '@fortawesome/free-solid-svg-icons/faFingerprint'
import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons/faWandMagicSparkles'
import { faCircleUser } from '@fortawesome/free-solid-svg-icons/faCircleUser'
import { faComment } from '@fortawesome/free-solid-svg-icons/faComment'
import { faKey } from '@fortawesome/free-solid-svg-icons/faKey'
import { faCircleNodes } from '@fortawesome/free-solid-svg-icons/faCircleNodes'
import { faBullseye } from '@fortawesome/free-solid-svg-icons/faBullseye'
import { faEyeSlash } from '@fortawesome/free-solid-svg-icons/faEyeSlash'
import { faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons/faUpRightFromSquare'
import { faShareNodes } from '@fortawesome/free-solid-svg-icons/faShareNodes'
import { faPaste } from '@fortawesome/free-solid-svg-icons/faPaste'
import { faKeyboard } from '@fortawesome/free-solid-svg-icons/faKeyboard'
import { faMoneyBill1 } from '@fortawesome/free-solid-svg-icons/faMoneyBill1'
import { faGears } from '@fortawesome/free-solid-svg-icons/faGears'
import { faTag } from '@fortawesome/free-solid-svg-icons/faTag'
import { faBank } from '@fortawesome/free-solid-svg-icons/faBank'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons/faChevronDown'
import { faChevronUp } from '@fortawesome/free-solid-svg-icons/faChevronUp'
import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons/faCircleExclamation'
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons/faCircleQuestion'
import { faEnvelope } from '@fortawesome/free-solid-svg-icons/faEnvelope'
import { faCircleArrowUp } from "@fortawesome/free-solid-svg-icons/faCircleArrowUp"
import { faCircleArrowDown } from "@fortawesome/free-solid-svg-icons/faCircleArrowDown"
import { faGlobe } from "@fortawesome/free-solid-svg-icons/faGlobe"
import { faCubes } from "@fortawesome/free-solid-svg-icons/faCubes"
import { faClock } from "@fortawesome/free-regular-svg-icons/faClock"
export type IconTypes = keyof typeof iconRegistry
// TODO remove need for manual iconregistry?
// would be best to just import all of them, i guess, or figure out something smart
export const iconRegistry = { faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe, faCubes, faClock }
interface IconProps extends TouchableOpacityProps {
/**
* The name of the icon
*/
icon: IconTypes
/**
* An optional tint color for the icon
*/
color?: ColorValue
/**
* An optional size for the icon. If not provided, the icon will be sized to the icon's resolution.
*/
size?: number
/**
* An inverse style with white icon on colored rounded background.
*/
inverse?: boolean
/**
* Style overrides for the icon image
*/
style?: StyleProp<ImageStyle>
/**
* Style overrides for the icon container
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Transform style
*/
transform?: string | Transform | undefined
/**
* An optional function to be called when the icon is pressed
*/
onPress?: TouchableOpacityProps["onPress"]
}
/**
* A component to render a registered icon.
* It is wrapped in a <TouchableOpacity /> if `onPress` is provided, otherwise a <View />.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Icon.md)
*/
export function Icon(props: IconProps) {
const {
icon,
color = useThemeColor('text'),
size,
inverse = false,
transform,
style: $imageStyleOverride,
containerStyle: $containerStyleOverride,
...WrapperProps
} = props
const isPressable = !!WrapperProps.onPress
const Wrapper: ComponentType<TouchableOpacityProps> = WrapperProps?.onPress
? TouchableOpacity
: View
return (
<Wrapper
accessibilityRole={isPressable ? "imagebutton" : undefined}
{...WrapperProps}
style={inverse ? [$inverseContainer, {backgroundColor: color}, $containerStyleOverride] : [$container, $containerStyleOverride]}
>
<FontAwesomeIcon
icon={iconRegistry[icon]}
size={size}
color={inverse ? 'white' : color as string}
transform={transform}
/>
</Wrapper>
)
}
const $imageStyle: ImageStyle = {
resizeMode: "contain",
}
const $container: ImageStyle = {
padding: spacing.extraSmall,
}
const $inverseContainer: ImageStyle = {
flex: 0,
borderRadius: spacing.small,
padding: spacing.extraSmall,
}

View File

@@ -0,0 +1,43 @@
import React, { FC, useState, useEffect } from 'react'
import { View } from 'react-native'
import { BottomModal, Icon, Text } from '../components'
import { spacing, useThemeColor } from '../theme'
type InfoModalProps = {
message: string
}
export const InfoModal: FC<InfoModalProps> = function ({ message }) {
const [isInfoVisible, setIsInfoVisible] = useState<boolean>(true)
// needed for info to re-appear
useEffect(() => {
setIsInfoVisible(true)
// setTimeout(() => setIsInfoVisible(false), 3000) //
}, [message])
const onClose = () => {
setIsInfoVisible(false)
}
const backgroundColor = useThemeColor('info')
const iconColor = useThemeColor('textDim')
return (
<BottomModal
isVisible={isInfoVisible}
onBackdropPress={() => onClose()}
onBackButtonPress={() => onClose()}
ContentComponent={
<View style={{ padding: spacing.small, flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
<Icon icon="faInfoCircle" size={spacing.large} color={iconColor} />
<Text style={{ marginHorizontal: spacing.extraSmall}}>{message}</Text>
</View>
}
>
</BottomModal>
)
}

344
src/components/ListItem.tsx Normal file
View File

@@ -0,0 +1,344 @@
import React from 'react'
import { verticalScale } from "@gocodingnow/rn-size-matters"
import { ReactElement } from "react"
import { ColorValue, useColorScheme } from "react-native"
import {
StyleProp,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from "react-native"
import { useThemeColor, spacing } from "../theme"
import { Icon, IconTypes } from "./Icon"
import { Text, TextProps } from "./Text"
export interface ListItemProps extends TouchableOpacityProps {
/**
* How tall the list item should be.
* Default: 56
*/
height?: number
/**
* Whether to show the top separator.
* Default: false
*/
topSeparator?: boolean
/**
* Whether to show the bottom separator.
* Default: false
*/
bottomSeparator?: boolean
/**
* Text to display if not using `tx` or nested components.
*/
text?: TextProps["text"]
/**
* Text which is looked up via i18n.
*/
tx?: TextProps["tx"]
/**
* Sub text to display if not using `tx` or nested components.
*/
subText?: TextProps["text"]
/**
* Sub text which is looked up via i18n.
*/
subTx?: TextProps["tx"]
/**
* Children components.
*/
children?: TextProps["children"]
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
txOptions?: TextProps["txOptions"]
/**
* Optional text ellipsize.
*/
textEllipsizeMode?: 'head' | 'middle'| 'tail'| 'clip'
/**
* Optional subtext ellipsize.
*/
subTextEllipsizeMode?: 'head' | 'middle'| 'tail'| 'clip'
/**
* Optional text style override.
*/
textStyle?: StyleProp<TextStyle>
/**
* Optional subtext style override.
*/
subTextStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the Text component.
*/
TextProps?: TextProps
/**
* Optional View container style override.
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Optional TouchableOpacity style override.
*/
style?: StyleProp<ViewStyle>
/**
* Icon that should appear on the left.
*/
leftIcon?: IconTypes
/**
* An optional tint color for the left icon
*/
leftIconColor?: string
leftIconTransform?: string
leftIconInverse?: boolean
/**
* Icon that should appear on the right.
*/
rightIcon?: IconTypes
/**
* An optional tint color for the right icon
*/
rightIconColor?: string
rightIconTransform?: string
rightIconInverse?: boolean
/**
* Right action custom ReactElement.
* Overrides `rightIcon`.
*/
RightComponent?: ReactElement
/**
* Left action custom ReactElement.
* Overrides `leftIcon`.
*/
LeftComponent?: ReactElement
/**
* Bottom ReactElement.
*
*/
BottomComponent?: ReactElement
}
interface ListItemActionProps {
icon: IconTypes | undefined
iconColor?: ColorValue
iconTransform?: string
iconInverse?: boolean
Component?: ReactElement
size: number
side: "left" | "right"
}
/**
* A styled row component that can be used in FlatList, SectionList, or by itself.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-ListItem.md)
*/
export const ListItem = function (props: ListItemProps) {
const colorScheme = useColorScheme()
const {
bottomSeparator,
children,
height = verticalScale(56),
LeftComponent,
leftIcon,
leftIconColor = useThemeColor('textDim'),
leftIconTransform,
leftIconInverse = false,
RightComponent,
rightIcon,
rightIconColor = useThemeColor('textDim'),
rightIconTransform,
rightIconInverse = false,
BottomComponent,
style,
text,
subText,
TextProps,
topSeparator,
tx,
subTx,
txOptions,
textEllipsizeMode,
subTextEllipsizeMode,
textStyle: $textStyleOverride,
subTextStyle: $subTextStyleOverride,
containerStyle: $containerStyleOverride,
...TouchableOpacityProps
} = props
const $textStyles = [$textStyle, $textStyleOverride, TextProps?.style]
const separatorColor = useThemeColor('separator')
const subTextColor = useThemeColor('textDim')
const $subTextStyles = [$subTextStyle, $subTextStyleOverride, TextProps?.style]
const $containerStyles = [
topSeparator && $separatorTop, { borderTopColor: separatorColor },
bottomSeparator && $separatorBottom, { borderBottomColor: separatorColor },
$containerStyleOverride,
]
const $touchableStyles = [$touchableStyle, { minHeight: height }, style]
return (
<View style={$containerStyles}>
<TouchableOpacity {...TouchableOpacityProps} style={$touchableStyles}>
<ListItemAction
side="left"
size={height}
icon={leftIcon}
iconColor={leftIconColor}
iconTransform={leftIconTransform}
iconInverse={leftIconInverse}
Component={LeftComponent}
/>
<View style={$subTextContainer}>
<>
{(subText || subTx) ? (
<>
{textEllipsizeMode ? (
<Text numberOfLines={1} ellipsizeMode={textEllipsizeMode} {...TextProps} tx={tx} text={text} txOptions={txOptions} style={$textStyles}>
{children}
</Text>
) : (
<Text {...TextProps} tx={tx} text={text} txOptions={txOptions} style={$textStyles}>
{children}
</Text>
)}
{subTextEllipsizeMode ? (
<Text numberOfLines={1} ellipsizeMode={subTextEllipsizeMode} {...TextProps} size="xs" tx={subTx} text={subText} txOptions={txOptions} style={[$subTextStyles, {color: subTextColor}]}>
</Text>
) : (
<Text {...TextProps} size="xs" tx={subTx} text={subText} txOptions={txOptions} style={[$subTextStyles, {color: subTextColor}]}>
</Text>
)}
</>
) : (
<>
{textEllipsizeMode ? (
<Text numberOfLines={1} ellipsizeMode={textEllipsizeMode} {...TextProps} tx={tx} text={text} txOptions={txOptions} style={$textStyles}>
{children}
</Text>
) : (
<Text {...TextProps} tx={tx} text={text} txOptions={txOptions} style={$textStyles}>
{children}
</Text>
)}
</>
)}
{(BottomComponent) && (
<View style={$bottomComponentContainer}>
{BottomComponent}
</View>
)}
</>
</View>
<ListItemAction
side="right"
size={height}
icon={rightIcon}
iconColor={rightIconColor}
iconTransform={rightIconTransform}
iconInverse={rightIconInverse}
Component={RightComponent}
/>
</TouchableOpacity>
</View>
)
}
function ListItemAction(props: ListItemActionProps) {
const { icon, Component, iconColor, iconTransform, iconInverse, size, side } = props
if (Component) return (
<View style={$componentContainer}>
{Component}
</View>
)
if (icon) {
return (
<Icon
size={spacing.medium}
icon={icon}
color={iconColor}
transform={iconTransform}
inverse={iconInverse}
containerStyle={[
$iconContainer,
side === "left" && $iconContainerLeft,
side === "right" && $iconContainerRight,
iconInverse === true && {marginRight: spacing.medium},
{ height: size },
]}
/>
)
}
return null
}
const $separatorTop: ViewStyle = {
borderTopWidth: 1,
}
const $separatorBottom: ViewStyle = {
borderBottomWidth: 1,
}
const $textStyle: TextStyle = {
// alignSelf: 'flex-start',
paddingVertical: spacing.extraSmall,
// alignSelf: "center",
textAlignVertical: 'center',
flexGrow: 1,
flexShrink: 1,
}
const $subTextContainer: ViewStyle = {
flex: 1,
flexDirection: 'column',
// borderColor: 'red',
// borderWidth: 1,
}
const $subTextStyle: TextStyle = {
flexGrow: 1,
flexShrink: 1,
paddingBottom: spacing.extraSmall,
}
const $touchableStyle: ViewStyle = {
flexDirection: "row",
alignItems: "flex-start",
}
const $componentContainer: ViewStyle = {
flex: 0,
alignSelf: 'center',
}
const $bottomComponentContainer: ViewStyle = {
flex: 0,
flexDirection: 'row',
paddingBottom: spacing.extraSmall,
}
const $iconContainer: ViewStyle = {
flex: 0,
alignSelf: 'center',
maxHeight: spacing.medium + spacing.extraSmall * 2,
}
const $iconContainerLeft: ViewStyle = {
marginEnd: spacing.small,
}
const $iconContainerRight: ViewStyle = {
marginStart: spacing.small,
}

View File

@@ -0,0 +1,28 @@
import * as React from 'react'
import { StyleSheet, View, ActivityIndicator, ViewProps, TextStyle, ColorValue, ViewStyle, StatusBar } from 'react-native'
import { useThemeColor, colors } from '../theme'
import { Text } from './Text'
import { spacing } from '../theme'
export function Loading(props: ViewProps & { statusMessage?: string, textStyle?: TextStyle, shiftedUp?: boolean }) {
const statusBarOnModalOpen = useThemeColor('statusBarOnLoading')
const loadingIndicator = useThemeColor('loadingIndicator')
return (
<View style={[StyleSheet.absoluteFillObject, $loading(props?.shiftedUp ?? false), props.style]}>
<StatusBar backgroundColor={props.style && props.style.backgroundColor ? props.style.backgroundColor : statusBarOnModalOpen } />
<ActivityIndicator color={loadingIndicator} animating size="large" />
{props.statusMessage && (<Text style={[{opacity: 1}, props.textStyle]} text={props.statusMessage}/>)}
</View>
)
}
const $loading = (shiftedUp = false) => ({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.dark.background,
opacity: 0.25,
zIndex: 9999,
// for cards that go up in the header, the loading should cover them
marginTop: shiftedUp ? -(spacing.extraLarge * 2 + spacing.small) : 0,
} satisfies ViewStyle)

View File

@@ -0,0 +1,82 @@
import React, {forwardRef} from 'react'
import {View, ViewStyle, TextInput, TextStyle} from 'react-native'
import {spacing, useThemeColor} from '../theme'
import {Button} from './Button'
import {Card} from './Card'
import {translate} from '../i18n'
interface MemoInputProps {
memo: string
setMemo: (memo: string) => void
disabled?: boolean
onMemoDone: () => void
onMemoEndEditing?: () => void
maxLength?: number
}
export const MemoInputCard = forwardRef<TextInput, MemoInputProps>((props, memoInputRef) => {
const {
memo,
setMemo,
disabled = true,
onMemoDone,
onMemoEndEditing = () => {},
maxLength = 200,
} = props
const placeholderTextColor = useThemeColor('textDim')
return (
<Card
style={$memoCard}
ContentComponent={
<View style={$memoContainer}>
<TextInput
ref={memoInputRef}
onChangeText={memo => setMemo(memo)}
onEndEditing={onMemoEndEditing}
value={`${memo}`}
style={$memoInput}
maxLength={maxLength}
keyboardType="default"
selectTextOnFocus={true}
placeholder={translate('sendScreen.memo')}
placeholderTextColor={placeholderTextColor}
editable={!disabled}
/>
<Button
preset="secondary"
style={$memoButton}
text="Done"
onPress={onMemoDone}
disabled={disabled}
/>
</View>
}
/>
)
})
const $memoContainer: ViewStyle = {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}
const $memoCard: ViewStyle = {
marginBottom: spacing.small,
minHeight: 80,
}
const $memoButton: ViewStyle = {
maxHeight: 50,
}
const $memoInput: TextStyle = {
flex: 1,
borderRadius: spacing.small,
fontSize: 16,
textAlignVertical: 'center',
marginRight: spacing.small,
}

View File

@@ -0,0 +1 @@
export const MintIcon = `<svg width="256px" height="256px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M10.8321 1.24802C11.5779 0.917327 12.4221 0.917327 13.1679 1.24802L21.7995 5.0754C23.7751 5.95141 23.1703 9 21.0209 9H2.97906C0.829669 9 0.224891 5.9514 2.20047 5.0754L10.8321 1.24802ZM12.3893 3.12765C12.1407 3.01742 11.8593 3.01742 11.6107 3.12765L3.41076 6.76352C3.31198 6.80732 3.34324 6.95494 3.45129 6.95494H20.5487C20.6568 6.95494 20.688 6.80732 20.5892 6.76352L12.3893 3.12765Z"></path> <path d="M2 22C2 21.4477 2.44772 21 3 21H21C21.5523 21 22 21.4477 22 22C22 22.5523 21.5523 23 21 23H3C2.44772 23 2 22.5523 2 22Z"></path> <path d="M11 19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V11C13 10.4477 12.5523 10 12 10C11.4477 10 11 10.4477 11 11V19Z"></path> <path d="M6 20C5.44772 20 5 19.5523 5 19L5 11C5 10.4477 5.44771 10 6 10C6.55228 10 7 10.4477 7 11L7 19C7 19.5523 6.55229 20 6 20Z"></path> <path d="M17 19C17 19.5523 17.4477 20 18 20C18.5523 20 19 19.5523 19 19V11C19 10.4477 18.5523 10 18 10C17.4477 10 17 10.4477 17 11V19Z" ></path> </g></svg>`

View File

@@ -0,0 +1 @@
export const NwcIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="128" height="120" viewBox="0 0 128 120" fill="none"><g clip-path="url(#a)"><path fill="#472459" d="M127.2 65.67c-5.13 5.11-12.23 3.88-17.35-1.25l-41.4-41.6 19.4-19.47a11.35 11.35 0 0 1 18.4 3.37l21.58 56.22a2.5 2.5 0 0 1-.57 2.67l-13.63 13.57 13.58-13.51Z"/><path fill="url(#b)" d="m109.85 64.42-54.16-54.4a13.13 13.13 0 0 0-18.57-.05L3.87 43.07a13.13 13.13 0 0 0-.05 18.57l54.16 54.4a13.13 13.13 0 0 0 18.57.04l10.7-10.64 3.12-3.12-10-10.03a19.04 19.04 0 0 1-23.83-2.56l-6.8-6.85a2.46 2.46 0 0 1 0-3.5l3.35-3.32-8.4-8.46a3.67 3.67 0 0 1-.34-4.9 3.57 3.57 0 0 1 5.29-.23l8.51 8.56 6.68-6.63-8.41-8.46a3.67 3.67 0 0 1-.33-4.9 3.58 3.58 0 0 1 5.3-.24l8.5 8.57 3.36-3.34a2.47 2.47 0 0 1 3.5.01l6.8 6.84a19.03 19.03 0 0 1 2.41 23.86l10 10.03 5.69-5.67 8.16-8.12 17.4-17.31c-5.15 5.11-12.25 3.88-17.36-1.25Z"/></g><defs><linearGradient id="b" x1="63.6" x2="63.6" y1="6.15" y2="119.91" gradientUnits="userSpaceOnUse"><stop stop-color="#FFCA4A"/><stop offset="1" stop-color="#F7931A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h128v119.91H0z"/></clipPath></defs></svg>`

View File

@@ -0,0 +1,23 @@
import { observer } from "mobx-react-lite"
import React from "react"
import { useStores } from "../models"
import { translate } from "../i18n"
import { AvatarHeader } from "./AvatarHeader"
export interface ProfileHeaderProps {
headerBg?: string
}
export const ProfileHeader = observer(function (props: ProfileHeaderProps) {
const { walletProfileStore } = useStores()
const { picture, nip05 } = walletProfileStore
return ( <AvatarHeader
text={nip05 || translate("common.notCreated")}
picture={picture}
fallbackIcon="faCircleUser"
headerBgColor={props.headerBg}
pictureHeight={walletProfileStore.isOwnProfile ? 90 : 96}
/>)
})

View File

@@ -0,0 +1,96 @@
import React, { useState } from 'react'
import { View } from 'react-native'
import type { TextStyle, ViewStyle } from 'react-native';
import { BottomModal } from './BottomModal';
import { Text } from './Text';
import { QRCodeBlock, QRCodeBlockTypes } from '../screens/Wallet/QRCode';
import { spacing, useThemeColor } from '../theme';
import { translate, TxKeyPath } from '../i18n';
import { Button } from './Button';
interface QRShareModalProps {
url: string,
isVisible?: boolean,
onClose?: () => void,
type: QRCodeBlockTypes
shareModalTitle?: string,
shareModalTx?: TxKeyPath,
subHeading?: string,
label?: string
labelTx?: TxKeyPath,
size?: number,
}
export const QRShareModal = (props: QRShareModalProps) => {
const labelText = useThemeColor('textDim')
const {
url,
isVisible = true,
onClose = () => {},
shareModalTitle,
shareModalTx,
subHeading,
type,
label,
labelTx,
size
} = props
let finalTx: TxKeyPath | undefined = shareModalTx
if (!shareModalTx && !shareModalTitle) finalTx = 'common.share'
return (
<BottomModal
isVisible={isVisible}
ContentComponent={
<>
<Text
tx={finalTx}
text={shareModalTitle}
style={{alignSelf: 'center', marginBottom: spacing.small}}
/>
<View style={$newContainer}>
<QRCodeBlock
qrCodeData={url.toString()}
title={subHeading || shareModalTitle || translate('common.share')}
type={type}
size={size}
/>
{(label || labelTx) &&
<Text
size="xxs"
style={{
color: labelText,
marginTop: spacing.medium,
alignSelf: 'center',
}}
text={label}
tx={labelTx && !label ? labelTx : undefined}
/>
}
<View style={$buttonContainer}>
<Button
onPress={props.onClose}
text='Close'
preset='tertiary'
/>
</View>
</View>
</>
}
onBackButtonPress={onClose}
onBackdropPress={onClose}
/>
)
}
const $newContainer: TextStyle = {
alignSelf: 'stretch',
}
const $buttonContainer: ViewStyle = {
flexDirection: 'row',
marginTop: spacing.medium,
justifyContent: 'center',
}

View File

@@ -0,0 +1 @@
export const ScanIcon = '<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g id="XMLID_1_"> <path id="XMLID_7_" d="M59.3,93.2c0-8.6,6-14.6,14.6-14.6h83.3V19.3H75.6C34.4,19.3,0,52,0,93.2v83.3h59.3V93.2z M437.3,19.3h-83.3 v59.3h82.5c8.6,0,15.5,6,15.5,14.6v83.3h59.3V93.2C512,52,478.5,19.3,437.3,19.3z M452.7,416.2c0,8.6-6,14.6-14.6,14.6h-83.3V491 h83.3c41.2,0,73.9-34.4,73.9-75.6v-82.5h-59.3V416.2z M73.9,431.7c-8.6,0-14.6-6-14.6-14.6v-82.5H0v82.5c0,41.2,34.4,75.6,75.6,75.6h82.5v-60.1H73.9V431.7z"/></g></svg>'

236
src/components/Screen.tsx Normal file
View File

@@ -0,0 +1,236 @@
import { useScrollToTop } from "@react-navigation/native"
import { useIsFocused } from '@react-navigation/native'
import React, { useRef, useState } from "react"
import {
KeyboardAvoidingView,
KeyboardAvoidingViewProps,
LayoutChangeEvent,
Platform,
ScrollView,
ScrollViewProps,
StyleProp,
View,
ViewStyle,
useColorScheme,
StatusBar,
StatusBarProps
} from "react-native"
import { useThemeColor, spacing } from "../theme"
import { ExtendedEdge, useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"
interface BaseScreenProps {
/**
* Children components.
*/
children?: React.ReactNode
/**
* Style for the outer content container useful for padding & margin.
*/
style?: StyleProp<ViewStyle>
/**
* Style for the inner content container useful for padding & margin.
*/
contentContainerStyle?: StyleProp<ViewStyle>
/**
* Override the default edges for the safe area.
*/
safeAreaEdges?: ExtendedEdge[]
/**
* Background color
*/
backgroundColor?: string
/**
* By how much should we offset the keyboard? Defaults to 0.
*/
keyboardOffset?: number
/**
* Pass any additional props directly to the KeyboardAvoidingView component.
*/
KeyboardAvoidingViewProps?: KeyboardAvoidingViewProps
}
interface FixedScreenProps extends BaseScreenProps {
preset?: "fixed"
}
interface ScrollScreenProps extends BaseScreenProps {
preset?: "scroll"
/**
* Should keyboard persist on screen tap. Defaults to handled.
* Only applies to scroll preset.
*/
keyboardShouldPersistTaps?: "handled" | "always" | "never"
/**
* Pass any additional props directly to the ScrollView component.
*/
ScrollViewProps?: ScrollViewProps
}
interface AutoScreenProps extends Omit<ScrollScreenProps, "preset"> {
preset?: "auto"
/**
* Threshold to trigger the automatic disabling/enabling of scroll ability.
* Defaults to `{ percent: 0.92 }`.
*/
scrollEnabledToggleThreshold?: { percent?: number; point?: number }
}
export type ScreenProps = ScrollScreenProps | FixedScreenProps | AutoScreenProps
const isIos = Platform.OS === "ios"
function isNonScrolling(preset?: ScreenProps["preset"]) {
return !preset || preset === "fixed"
}
function useAutoPreset(props: AutoScreenProps) {
const { preset, scrollEnabledToggleThreshold } = props
const { percent = 0.92, point = 0 } = scrollEnabledToggleThreshold || {}
const scrollViewHeight = useRef(null)
const scrollViewContentHeight = useRef(null)
const [scrollEnabled, setScrollEnabled] = useState(true)
function updateScrollState() {
if (scrollViewHeight.current === null || scrollViewContentHeight.current === null) return
// check whether content fits the screen then toggle scroll state according to it
const contentFitsScreen = (function () {
if (point) {
return scrollViewContentHeight.current < scrollViewHeight.current - point
} else {
return scrollViewContentHeight.current < scrollViewHeight.current * percent
}
})()
// content is less than the size of the screen, so we can disable scrolling
if (scrollEnabled && contentFitsScreen) setScrollEnabled(false)
// content is greater than the size of the screen, so let's enable scrolling
if (!scrollEnabled && !contentFitsScreen) setScrollEnabled(true)
}
function onContentSizeChange(w: number, h: number) {
// update scroll-view content height
scrollViewContentHeight.current = h
updateScrollState()
}
function onLayout(e: LayoutChangeEvent) {
const { height } = e.nativeEvent.layout
// update scroll-view height
scrollViewHeight.current = height
updateScrollState()
}
// update scroll state on every render
if (preset === "auto") updateScrollState()
return {
scrollEnabled: preset === "auto" ? scrollEnabled : true,
onContentSizeChange,
onLayout,
}
}
function ScreenWithoutScrolling(props: ScreenProps) {
const { style, contentContainerStyle, children } = props
return (
<View style={[$outerStyle, style]}>
<View style={[$innerStyle, contentContainerStyle]}>{children}</View>
</View>
)
}
function ScreenWithScrolling(props: ScreenProps) {
const {
children,
keyboardShouldPersistTaps = "handled",
contentContainerStyle,
ScrollViewProps,
style,
} = props as ScrollScreenProps
const ref = useRef<ScrollView>()
const { scrollEnabled, onContentSizeChange, onLayout } = useAutoPreset(props as AutoScreenProps)
// Add native behavior of pressing the active tab to scroll to the top of the content
// More info at: https://reactnavigation.org/docs/use-scroll-to-top/
useScrollToTop(ref)
return (
<ScrollView
{...{ keyboardShouldPersistTaps, scrollEnabled, ref }}
{...ScrollViewProps}
onLayout={(e) => {
onLayout(e)
ScrollViewProps?.onLayout?.(e)
}}
onContentSizeChange={(w: number, h: number) => {
onContentSizeChange(w, h)
ScrollViewProps?.onContentSizeChange?.(w, h)
}}
style={[$outerStyle, ScrollViewProps?.style, style]}
contentContainerStyle={[
$innerStyle,
ScrollViewProps?.contentContainerStyle,
contentContainerStyle,
]}
>
{children}
</ScrollView>
)
}
export function Screen(props: ScreenProps) {
const {
backgroundColor = useThemeColor('background'),
KeyboardAvoidingViewProps,
keyboardOffset = 0,
safeAreaEdges,
} = props
const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges)
return (
<View style={[$containerStyle, { backgroundColor }, $containerInsets]}>
<KeyboardAvoidingView
behavior={isIos ? "padding" : undefined}
keyboardVerticalOffset={keyboardOffset}
{...KeyboardAvoidingViewProps}
style={[$keyboardAvoidingViewStyle, KeyboardAvoidingViewProps?.style]}
>
{isNonScrolling(props.preset) ? (
<ScreenWithoutScrolling {...props} />
) : (
<ScreenWithScrolling {...props} />
)}
</KeyboardAvoidingView>
</View>
)
}
const $containerStyle: ViewStyle = {
flex: 1,
height: "100%",
width: "100%",
}
const $keyboardAvoidingViewStyle: ViewStyle = {
flex: 1,
}
const $outerStyle: ViewStyle = {
flex: 1,
height: "100%",
width: "100%",
}
const $innerStyle: ViewStyle = {
justifyContent: "flex-start",
alignItems: "stretch",
}

112
src/components/Text.tsx Normal file
View File

@@ -0,0 +1,112 @@
import { moderateVerticalScale } from "@gocodingnow/rn-size-matters"
import { I18nOptions } from "i18n-js"
import React from "react"
import { StyleProp, Text as RNText, TextProps as RNTextProps, TextStyle } from "react-native"
import { translate, TxKeyPath } from "../i18n"
import { useThemeColor, typography } from "../theme"
type Sizes = keyof typeof $sizeStyles
type Weights = keyof typeof typography.primary
type Presets = keyof typeof $presets
export interface TextProps extends RNTextProps {
/**
* Text which is looked up via i18n.
*/
tx?: TxKeyPath
/**
* The text to display if not using `tx` or nested components.
*/
text?: string
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
txOptions?: I18nOptions
/**
* An optional style override useful for padding & margin.
*/
style?: StyleProp<TextStyle>
/**
* One of the different types of text presets.
*/
preset?: Presets
/**
* Text weight modifier.
*/
weight?: Weights
/**
* Text size modifier.
*/
size?: Sizes
/**
* Children components.
*/
children?: React.ReactNode
}
/**
* For your text displaying needs.
* This component is a HOC over the built-in React Native one.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Text.md)
*/
export function Text(props: TextProps) {
const { weight, size, tx, txOptions, text, children, style: $styleOverride, ...rest } = props
const i18nText = tx && translate(tx, txOptions)
const content = i18nText || text || children
const preset: Presets = $presets[props.preset] ? props.preset : "default"
const $styles = [
$presets[preset],
weight ? {fontFamily: weight} : null,
size ? $sizeStyles[size] : null,
{ color: useThemeColor('text') },
$styleOverride,
]
const $baseFontFamily = { fontFamily: typography.primary } as TextStyle
return (
<RNText {...rest} style={[$baseFontFamily, $styles]}>
{content}
</RNText>
)
}
export const $sizeStyles = {
xxl: { fontSize: moderateVerticalScale(36), lineHeight: moderateVerticalScale(44) } as TextStyle,
xl: { fontSize: moderateVerticalScale(24), lineHeight: moderateVerticalScale(34) } as TextStyle,
lg: { fontSize: moderateVerticalScale(20), lineHeight: moderateVerticalScale(32) } as TextStyle,
md: { fontSize: moderateVerticalScale(18), lineHeight: moderateVerticalScale(26) } as TextStyle,
sm: { fontSize: moderateVerticalScale(16), lineHeight: moderateVerticalScale(24) } as TextStyle,
xs: { fontSize: moderateVerticalScale(14), lineHeight: moderateVerticalScale(21) } as TextStyle,
xxs: { fontSize: moderateVerticalScale(12), lineHeight: moderateVerticalScale(18) } as TextStyle,
}
// does not work
/* const $fontWeightStyles = Object.entries(typography.primary).reduce((acc, [weight, fontFamily]) => {
return { ...acc, [weight]: { fontFamily } }
}, {}) as Record<Weights, TextStyle> */
const $baseStyle: StyleProp<TextStyle> = [
$sizeStyles.sm,
{fontFamily: typography.primary?.normal}
]
const $presets = {
default: $baseStyle,
bold: [$baseStyle, {fontFamily: typography.primary?.medium}] as StyleProp<TextStyle>,
heading: [$baseStyle, $sizeStyles.xxl, {fontFamily: typography.primary?.medium}] as StyleProp<TextStyle>,
subheading: [$baseStyle, $sizeStyles.lg] as StyleProp<TextStyle>,
formLabel: [$baseStyle] as StyleProp<TextStyle>,
formHelper: [$baseStyle, $sizeStyles.sm, {fontFamily: typography.primary?.light}] as StyleProp<TextStyle>,
}

View File

@@ -0,0 +1,275 @@
import React, { ComponentType, forwardRef, Ref, useImperativeHandle, useRef } from "react"
import {
StyleProp,
TextInput,
TextInputProps,
TextStyle,
TouchableOpacity,
View,
ViewStyle,
} from "react-native"
import { isRTL, translate } from "../i18n"
import { colors, spacing, typography } from "../theme"
import { Text, TextProps } from "./Text"
export interface TextFieldAccessoryProps {
style: StyleProp<any>
status: TextFieldProps["status"]
multiline: boolean
editable: boolean
}
export interface TextFieldProps extends Omit<TextInputProps, "ref"> {
/**
* A style modifier for different input states.
*/
status?: "error" | "disabled"
/**
* The label text to display if not using `labelTx`.
*/
label?: TextProps["text"]
/**
* Label text which is looked up via i18n.
*/
labelTx?: TextProps["tx"]
/**
* Optional label options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
labelTxOptions?: TextProps["txOptions"]
/**
* Pass any additional props directly to the label Text component.
*/
LabelTextProps?: TextProps
/**
* The helper text to display if not using `helperTx`.
*/
helper?: TextProps["text"]
/**
* Helper text which is looked up via i18n.
*/
helperTx?: TextProps["tx"]
/**
* Optional helper options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
helperTxOptions?: TextProps["txOptions"]
/**
* Pass any additional props directly to the helper Text component.
*/
HelperTextProps?: TextProps
/**
* The placeholder text to display if not using `placeholderTx`.
*/
placeholder?: TextProps["text"]
/**
* Placeholder text which is looked up via i18n.
*/
placeholderTx?: TextProps["tx"]
/**
* Optional placeholder options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
placeholderTxOptions?: TextProps["txOptions"]
/**
* Optional input style override.
*/
style?: StyleProp<TextStyle>
/**
* Style overrides for the container
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Style overrides for the input wrapper
*/
inputWrapperStyle?: StyleProp<ViewStyle>
/**
* An optional component to render on the right side of the input.
* Example: `RightAccessory={(props) => <Icon icon="ladybug" containerStyle={props.style} color={props.editable ? colors.textDim : colors.text} />}`
* Note: It is a good idea to memoize this.
*/
RightAccessory?: ComponentType<TextFieldAccessoryProps>
/**
* An optional component to render on the left side of the input.
* Example: `LeftAccessory={(props) => <Icon icon="ladybug" containerStyle={props.style} color={props.editable ? colors.textDim : colors.text} />}`
* Note: It is a good idea to memoize this.
*/
LeftAccessory?: ComponentType<TextFieldAccessoryProps>
}
/**
* A component that allows for the entering and editing of text.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-TextField.md)
*/
export const TextField = forwardRef(function TextField(props: TextFieldProps, ref: Ref<TextInput>) {
const {
labelTx,
label,
labelTxOptions,
placeholderTx,
placeholder,
placeholderTxOptions,
helper,
helperTx,
helperTxOptions,
status,
RightAccessory,
LeftAccessory,
HelperTextProps,
LabelTextProps,
style: $inputStyleOverride,
containerStyle: $containerStyleOverride,
inputWrapperStyle: $inputWrapperStyleOverride,
...TextInputProps
} = props
const input = useRef<TextInput>()
const disabled = TextInputProps.editable === false || status === "disabled"
const placeholderContent = placeholderTx
? translate(placeholderTx, placeholderTxOptions)
: placeholder
const $containerStyles = [$containerStyleOverride]
const $labelStyles = [$labelStyle, LabelTextProps?.style]
const $inputWrapperStyles = [
$inputWrapperStyle,
status === "error" && { borderColor: colors.light.error },
TextInputProps.multiline && { minHeight: 112 },
LeftAccessory && { paddingStart: 0 },
RightAccessory && { paddingEnd: 0 },
$inputWrapperStyleOverride,
]
const $inputStyles = [
$inputStyle,
disabled && { color: colors.light.textDim },
isRTL && { textAlign: "right" as TextStyle["textAlign"] },
TextInputProps.multiline && { height: "auto" },
$inputStyleOverride,
]
const $helperStyles = [
$helperStyle,
status === "error" && { color: colors.light.error },
HelperTextProps?.style,
]
function focusInput() {
if (disabled) return
input.current?.focus()
}
useImperativeHandle(ref, () => input.current)
return (
<TouchableOpacity
activeOpacity={1}
style={$containerStyles}
onPress={focusInput}
accessibilityState={{ disabled }}
>
{!!(label || labelTx) && (
<Text
preset="formLabel"
text={label}
tx={labelTx}
txOptions={labelTxOptions}
{...LabelTextProps}
style={$labelStyles}
/>
)}
<View style={$inputWrapperStyles}>
{!!LeftAccessory && (
<LeftAccessory
style={$leftAccessoryStyle}
status={status}
editable={!disabled}
multiline={TextInputProps.multiline}
/>
)}
<TextInput
ref={input}
underlineColorAndroid={colors.light.transparent}
textAlignVertical="top"
placeholder={placeholderContent}
placeholderTextColor={colors.light.textDim}
{...TextInputProps}
editable={!disabled}
style={$inputStyles}
/>
{!!RightAccessory && (
<RightAccessory
style={$rightAccessoryStyle}
status={status}
editable={!disabled}
multiline={TextInputProps.multiline}
/>
)}
</View>
{!!(helper || helperTx) && (
<Text
preset="formHelper"
text={helper}
tx={helperTx}
txOptions={helperTxOptions}
{...HelperTextProps}
style={$helperStyles}
/>
)}
</TouchableOpacity>
)
})
const $labelStyle: TextStyle = {
marginBottom: spacing.extraSmall,
}
const $inputWrapperStyle: ViewStyle = {
flexDirection: "row",
alignItems: "flex-start",
borderWidth: 1,
borderRadius: 4,
backgroundColor: colors.palette.neutral200,
borderColor: colors.palette.neutral400,
overflow: "hidden",
}
const $inputStyle: TextStyle = {
flex: 1,
alignSelf: "stretch",
fontFamily: typography.primary?.normal,
color: colors.light.text,
fontSize: 16,
height: 24,
// https://github.com/facebook/react-native/issues/21720#issuecomment-532642093
paddingVertical: 0,
paddingHorizontal: 0,
marginVertical: spacing.extraSmall,
marginHorizontal: spacing.small,
}
const $helperStyle: TextStyle = {
marginTop: spacing.extraSmall,
}
const $rightAccessoryStyle: ViewStyle = {
marginEnd: spacing.extraSmall,
height: 40,
justifyContent: "center",
alignItems: "center",
}
const $leftAccessoryStyle: ViewStyle = {
marginStart: spacing.extraSmall,
height: 40,
justifyContent: "center",
alignItems: "center",
}

655
src/components/Toggle.tsx Normal file
View File

@@ -0,0 +1,655 @@
import React, { ComponentType, FC, useMemo } from "react"
import {
GestureResponderEvent,
Image,
ImageStyle,
StyleProp,
SwitchProps,
TextInputProps,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from "react-native"
import Animated, { useAnimatedStyle, withTiming } from "react-native-reanimated"
import { colors, spacing } from "../theme"
import { iconRegistry, IconTypes } from "./Icon"
import { Text, TextProps } from "./Text"
type Variants = "checkbox" | "switch" | "radio"
interface BaseToggleProps extends Omit<TouchableOpacityProps, "style"> {
/**
* The variant of the toggle.
* Options: "checkbox", "switch", "radio"
* Default: "checkbox"
*/
variant?: unknown
/**
* A style modifier for different input states.
*/
status?: "error" | "disabled"
/**
* If false, input is not editable. The default value is true.
*/
editable?: TextInputProps["editable"]
/**
* The value of the field. If true the component will be turned on.
*/
value?: boolean
/**
* Invoked with the new value when the value changes.
*/
onValueChange?: SwitchProps["onValueChange"]
/**
* Style overrides for the container
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Style overrides for the input wrapper
*/
inputWrapperStyle?: StyleProp<ViewStyle>
/**
* Optional input wrapper style override.
* This gives the inputs their size, shape, "off" background-color, and outer border.
*/
inputOuterStyle?: ViewStyle
/**
* Optional input style override.
* This gives the inputs their inner characteristics and "on" background-color.
*/
inputInnerStyle?: ViewStyle
/**
* The position of the label relative to the action component.
* Default: right
*/
labelPosition?: "left" | "right"
/**
* The label text to display if not using `labelTx`.
*/
label?: TextProps["text"]
/**
* Label text which is looked up via i18n.
*/
labelTx?: TextProps["tx"]
/**
* Optional label options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
labelTxOptions?: TextProps["txOptions"]
/**
* Style overrides for label text.
*/
labelStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the label Text component.
*/
LabelTextProps?: TextProps
/**
* The helper text to display if not using `helperTx`.
*/
helper?: TextProps["text"]
/**
* Helper text which is looked up via i18n.
*/
helperTx?: TextProps["tx"]
/**
* Optional helper options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
helperTxOptions?: TextProps["txOptions"]
/**
* Pass any additional props directly to the helper Text component.
*/
HelperTextProps?: TextProps
}
interface CheckboxToggleProps extends BaseToggleProps {
variant?: "checkbox"
/**
* Optional style prop that affects the Image component.
*/
inputDetailStyle?: ImageStyle
/**
* Checkbox-only prop that changes the icon used for the "on" state.
*/
checkboxIcon?: IconTypes
}
interface RadioToggleProps extends BaseToggleProps {
variant?: "radio"
/**
* Optional style prop that affects the dot View.
*/
inputDetailStyle?: ViewStyle
}
interface SwitchToggleProps extends BaseToggleProps {
variant?: "switch"
/**
* Switch-only prop that adds a text/icon label for on/off states.
*/
switchAccessibilityMode?: "text" | "icon"
/**
* Optional style prop that affects the knob View.
* Note: `width` and `height` rules should be points (numbers), not percentages.
*/
inputDetailStyle?: Omit<ViewStyle, "width" | "height"> & { width?: number; height?: number }
}
export type ToggleProps = CheckboxToggleProps | RadioToggleProps | SwitchToggleProps
interface ToggleInputProps {
on: boolean
status: BaseToggleProps["status"]
disabled: boolean
outerStyle: ViewStyle
innerStyle: ViewStyle
detailStyle: Omit<ViewStyle & ImageStyle, "overflow">
switchAccessibilityMode?: SwitchToggleProps["switchAccessibilityMode"]
checkboxIcon?: CheckboxToggleProps["checkboxIcon"]
}
/**
* Renders a boolean input.
* This is a controlled component that requires an onValueChange callback that updates the value prop in order for the component to reflect user actions. If the value prop is not updated, the component will continue to render the supplied value prop instead of the expected result of any user actions.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Toggle.md)
*/
export function Toggle(props: ToggleProps) {
const {
variant = "checkbox",
editable = true,
status,
value,
onPress,
onValueChange,
labelPosition = "right",
helper,
helperTx,
helperTxOptions,
HelperTextProps,
containerStyle: $containerStyleOverride,
inputWrapperStyle: $inputWrapperStyleOverride,
...WrapperProps
} = props
const { switchAccessibilityMode } = props as SwitchToggleProps
const { checkboxIcon } = props as CheckboxToggleProps
const disabled = editable === false || status === "disabled" || props.disabled
const Wrapper = useMemo<ComponentType<TouchableOpacityProps>>(
() => (disabled ? View : TouchableOpacity),
[disabled],
)
const ToggleInput = useMemo(() => ToggleInputs[variant] || (() => null), [variant])
const $containerStyles = [$containerStyleOverride]
const $inputWrapperStyles = [$inputWrapper, $inputWrapperStyleOverride]
const $helperStyles = [
$helper,
status === "error" && { color: colors.error },
HelperTextProps?.style,
]
function handlePress(e: GestureResponderEvent) {
if (disabled) return
onValueChange?.(!value)
onPress?.(e)
}
return (
<Wrapper
activeOpacity={1}
accessibilityRole={variant}
accessibilityState={{ checked: value, disabled }}
{...WrapperProps}
style={$containerStyles}
onPress={handlePress}
>
<View style={$inputWrapperStyles}>
{labelPosition === "left" && <FieldLabel {...props} labelPosition={labelPosition} />}
<ToggleInput
on={value}
disabled={disabled}
status={status}
outerStyle={props.inputOuterStyle}
innerStyle={props.inputInnerStyle}
detailStyle={props.inputDetailStyle}
switchAccessibilityMode={switchAccessibilityMode}
checkboxIcon={checkboxIcon}
/>
{labelPosition === "right" && <FieldLabel {...props} labelPosition={labelPosition} />}
</View>
{!!(helper || helperTx) && (
<Text
preset="formHelper"
text={helper}
tx={helperTx}
txOptions={helperTxOptions}
{...HelperTextProps}
style={$helperStyles}
/>
)}
</Wrapper>
)
}
const ToggleInputs: Record<Variants, FC<ToggleInputProps>> = {
checkbox: Checkbox,
switch: Switch,
radio: Radio,
}
function Checkbox(props: ToggleInputProps) {
const {
on,
status,
disabled,
checkboxIcon,
outerStyle: $outerStyleOverride,
innerStyle: $innerStyleOverride,
detailStyle: $detailStyleOverride,
} = props
const offBackgroundColor = [
disabled && colors.palette.neutral400,
status === "error" && colors.errorBackground,
colors.palette.neutral200,
].filter(Boolean)[0]
const outerBorderColor = [
disabled && colors.palette.neutral400,
status === "error" && colors.error,
!on && colors.palette.neutral800,
colors.palette.secondary500,
].filter(Boolean)[0]
const onBackgroundColor = [
disabled && colors.transparent,
status === "error" && colors.errorBackground,
colors.palette.secondary500,
].filter(Boolean)[0]
const iconTintColor = [
disabled && colors.palette.neutral600,
status === "error" && colors.error,
colors.palette.accent100,
].filter(Boolean)[0]
return (
<View
style={[
$inputOuterVariants.checkbox,
{ backgroundColor: offBackgroundColor, borderColor: outerBorderColor },
$outerStyleOverride,
]}
>
<Animated.View
style={[
$checkboxInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
useAnimatedStyle(() => ({ opacity: withTiming(on ? 1 : 0) }), [on]),
]}
>
<Image
source={iconRegistry[checkboxIcon] || iconRegistry.check}
style={[$checkboxDetail, { tintColor: iconTintColor }, $detailStyleOverride]}
/>
</Animated.View>
</View>
)
}
function Radio(props: ToggleInputProps) {
const {
on,
status,
disabled,
outerStyle: $outerStyleOverride,
innerStyle: $innerStyleOverride,
detailStyle: $detailStyleOverride,
} = props
const offBackgroundColor = [
disabled && colors.palette.neutral400,
status === "error" && colors.errorBackground,
colors.palette.neutral200,
].filter(Boolean)[0]
const outerBorderColor = [
disabled && colors.palette.neutral400,
status === "error" && colors.error,
!on && colors.palette.neutral800,
colors.palette.secondary500,
].filter(Boolean)[0]
const onBackgroundColor = [
disabled && colors.transparent,
status === "error" && colors.errorBackground,
colors.palette.neutral100,
].filter(Boolean)[0]
const dotBackgroundColor = [
disabled && colors.palette.neutral600,
status === "error" && colors.error,
colors.palette.secondary500,
].filter(Boolean)[0]
return (
<View
style={[
$inputOuterVariants.radio,
{ backgroundColor: offBackgroundColor, borderColor: outerBorderColor },
$outerStyleOverride,
]}
>
<Animated.View
style={[
$radioInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
useAnimatedStyle(() => ({ opacity: withTiming(on ? 1 : 0) }), [on]),
]}
>
<View
style={[$radioDetail, { backgroundColor: dotBackgroundColor }, $detailStyleOverride]}
/>
</Animated.View>
</View>
)
}
function Switch(props: ToggleInputProps) {
const {
on,
status,
disabled,
outerStyle: $outerStyleOverride,
innerStyle: $innerStyleOverride,
detailStyle: $detailStyleOverride,
} = props
const knobSizeFallback = 2
const knobWidth = [$detailStyleOverride?.width, $switchDetail?.width, knobSizeFallback].find(
(v) => typeof v === "number",
)
const knobHeight = [$detailStyleOverride?.height, $switchDetail?.height, knobSizeFallback].find(
(v) => typeof v === "number",
)
const offBackgroundColor = [
disabled && colors.palette.neutral400,
status === "error" && colors.errorBackground,
colors.palette.neutral300,
].filter(Boolean)[0]
const onBackgroundColor = [
disabled && colors.transparent,
status === "error" && colors.errorBackground,
colors.palette.secondary500,
].filter(Boolean)[0]
const knobBackgroundColor = (function () {
if (on) {
return [
$detailStyleOverride?.backgroundColor,
status === "error" && colors.error,
disabled && colors.palette.neutral600,
colors.palette.neutral100,
].filter(Boolean)[0]
} else {
return [
$innerStyleOverride?.backgroundColor,
disabled && colors.palette.neutral600,
status === "error" && colors.error,
colors.palette.neutral200,
].filter(Boolean)[0]
}
})()
const $animatedSwitchKnob = useAnimatedStyle(() => {
const offsetLeft = ($innerStyleOverride?.paddingStart ||
$innerStyleOverride?.paddingLeft ||
$switchInner?.paddingStart ||
$switchInner?.paddingLeft ||
0) as number
const offsetRight = ($innerStyleOverride?.paddingEnd ||
$innerStyleOverride?.paddingRight ||
$switchInner?.paddingEnd ||
$switchInner?.paddingRight ||
0) as number
const start = withTiming(on ? "100%" : "0%")
const marginStart = withTiming(on ? -(knobWidth || 0) - offsetRight : 0 + offsetLeft)
return { start, marginStart }
}, [on, knobWidth])
return (
<View
style={[
$inputOuterVariants.switch,
{ backgroundColor: offBackgroundColor },
$outerStyleOverride,
]}
>
<Animated.View
style={[
$switchInner,
{ backgroundColor: onBackgroundColor },
$innerStyleOverride,
useAnimatedStyle(() => ({ opacity: withTiming(on ? 1 : 0) }), [on]),
]}
/>
<SwitchAccessibilityLabel {...props} role="on" />
<SwitchAccessibilityLabel {...props} role="off" />
<Animated.View
style={[
$switchDetail,
$detailStyleOverride,
$animatedSwitchKnob,
{ width: knobWidth, height: knobHeight },
{ backgroundColor: knobBackgroundColor },
]}
/>
</View>
)
}
function SwitchAccessibilityLabel(props: ToggleInputProps & { role: "on" | "off" }) {
const { on, disabled, status, switchAccessibilityMode, role, innerStyle, detailStyle } = props
if (!switchAccessibilityMode) return null
const shouldLabelBeVisible = (on && role === "on") || (!on && role === "off")
const $switchAccessibilityStyle = [
$switchAccessibility,
role === "off" && { end: "5%" },
role === "on" && { left: "5%" },
]
const color = (function () {
if (disabled) return colors.palette.neutral600
if (status === "error") return colors.error
if (!on) return innerStyle?.backgroundColor || colors.palette.secondary500
return detailStyle?.backgroundColor || colors.palette.neutral100
})()
return (
<View style={$switchAccessibilityStyle}>
{switchAccessibilityMode === "text" && shouldLabelBeVisible && (
<View
style={[
role === "on" && $switchAccessibilityLine,
role === "on" && { backgroundColor: color },
role === "off" && $switchAccessibilityCircle,
role === "off" && { borderColor: color },
]}
/>
)}
{switchAccessibilityMode === "icon" && shouldLabelBeVisible && (
<Image
style={[$switchAccessibilityIcon, { tintColor: color }]}
source={role === "off" ? iconRegistry.hidden : iconRegistry.view}
/>
)}
</View>
)
}
function FieldLabel(props: BaseToggleProps) {
const {
status,
label,
labelTx,
labelTxOptions,
LabelTextProps,
labelPosition,
labelStyle: $labelStyleOverride,
} = props
if (!label && !labelTx && !LabelTextProps?.children) return null
const $labelStyle = [
$label,
status === "error" && { color: colors.error },
labelPosition === "right" && $labelRight,
labelPosition === "left" && $labelLeft,
$labelStyleOverride,
LabelTextProps?.style,
]
return (
<Text
preset="formLabel"
text={label}
tx={labelTx}
txOptions={labelTxOptions}
{...LabelTextProps}
style={$labelStyle}
/>
)
}
const $inputWrapper: ViewStyle = {
flexDirection: "row",
alignItems: "center",
}
const $inputOuterBase: ViewStyle = {
height: 24,
width: 24,
borderWidth: 2,
alignItems: "center",
overflow: "hidden",
flexGrow: 0,
flexShrink: 0,
justifyContent: "space-between",
flexDirection: "row",
}
const $inputOuterVariants: Record<Variants, StyleProp<ViewStyle>> = {
checkbox: [$inputOuterBase, { borderRadius: 4 }],
radio: [$inputOuterBase, { borderRadius: 12 }],
switch: [$inputOuterBase, { height: 32, width: 56, borderRadius: 16, borderWidth: 0 }],
}
const $checkboxInner: ViewStyle = {
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}
const $checkboxDetail: ImageStyle = {
width: 20,
height: 20,
resizeMode: "contain",
}
const $radioInner: ViewStyle = {
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}
const $radioDetail: ViewStyle = {
width: 12,
height: 12,
borderRadius: 6,
}
const $switchInner: ViewStyle = {
width: "100%",
height: "100%",
alignItems: "center",
borderColor: colors.transparent,
overflow: "hidden",
position: "absolute",
paddingStart: 4,
paddingEnd: 4,
}
const $switchDetail: SwitchToggleProps["inputDetailStyle"] = {
borderRadius: 12,
position: "absolute",
width: 24,
height: 24,
}
const $helper: TextStyle = {
marginTop: spacing.extraSmall,
}
const $label: TextStyle = {
flex: 1,
}
const $labelRight: TextStyle = {
marginStart: spacing.medium,
}
const $labelLeft: TextStyle = {
marginEnd: spacing.medium,
}
const $switchAccessibility: TextStyle = {
width: "40%",
justifyContent: "center",
alignItems: "center",
}
const $switchAccessibilityIcon: ImageStyle = {
width: 14,
height: 14,
resizeMode: "contain",
}
const $switchAccessibilityLine: ViewStyle = {
width: 2,
height: 12,
}
const $switchAccessibilityCircle: ViewStyle = {
borderWidth: 2,
width: 12,
height: 12,
borderRadius: 6,
}

View File

@@ -0,0 +1 @@
export const UsdIcon = `<svg width="256px" height="256px" viewBox="2 2 20.00 20.00" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(2.16,2.16), scale(0.82)"><rect x="0" y="0" width="24.00" height="24.00" rx="12" fill="#ffffff" strokewidth="0"></rect></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12.75 6C12.75 5.58579 12.4142 5.25 12 5.25C11.5858 5.25 11.25 5.58579 11.25 6V6.31673C9.61957 6.60867 8.25 7.83361 8.25 9.5C8.25 11.4172 10.0628 12.75 12 12.75C13.3765 12.75 14.25 13.6557 14.25 14.5C14.25 15.3443 13.3765 16.25 12 16.25C10.6235 16.25 9.75 15.3443 9.75 14.5C9.75 14.0858 9.41421 13.75 9 13.75C8.58579 13.75 8.25 14.0858 8.25 14.5C8.25 16.1664 9.61957 17.3913 11.25 17.6833V18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V17.6833C14.3804 17.3913 15.75 16.1664 15.75 14.5C15.75 12.5828 13.9372 11.25 12 11.25C10.6235 11.25 9.75 10.3443 9.75 9.5C9.75 8.65573 10.6235 7.75 12 7.75C13.3765 7.75 14.25 8.65573 14.25 9.5C14.25 9.91421 14.5858 10.25 15 10.25C15.4142 10.25 15.75 9.91421 15.75 9.5C15.75 7.83361 14.3804 6.60867 12.75 6.31673V6Z" fill="#599D52"></path> </g></svg>`

22
src/components/index.ts Normal file
View File

@@ -0,0 +1,22 @@
export * from "./AutoImage"
export * from "./Button"
export * from "./Card"
export * from "./Header"
export * from "./Icon"
export * from "./ListItem"
export * from "./Screen"
export * from "./Text"
export * from "./TextField"
export * from "./Toggle"
export * from "./EmptyState"
export * from "./ErrorModal"
export * from "./InfoModal"
export * from "./BottomModal"
export * from "./Loading"
export * from "./ScanIcon"
export * from "./BtcIcon"
export * from "./UsdIcon"
export * from "./EurIcon"
export * from "./MintIcon"
export * from "./HeaderBg"
export * from "./NwcIcon"

26
src/config/config.base.ts Normal file
View File

@@ -0,0 +1,26 @@
export interface ConfigBaseProps {
persistNavigation: "always" | "dev" | "prod" | "never"
catchErrors: "always" | "dev" | "prod" | "never"
exitRoutes: string[]
}
export type PersistNavigationConfig = ConfigBaseProps["persistNavigation"]
const BaseConfig: ConfigBaseProps = {
// This feature is particularly useful in development mode, but
// can be used in production as well if you prefer.
persistNavigation: "dev",
/**
* Only enable if we're catching errors in the right environment
*/
catchErrors: "always",
/**
* This is a list of all the route names that will exit the app if the back button
* is pressed while in that screen. Only affects Android.
*/
exitRoutes: ["Welcome"],
}
export default BaseConfig

10
src/config/config.dev.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* These are configuration settings for the dev environment.
*
* Do not include API secrets in this file or anywhere in your JS.
*
* https://reactnative.dev/docs/security#storing-sensitive-info
*/
export default {
API_URL: "https://api.rss2json.com/v1/",
}

10
src/config/config.prod.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* These are configuration settings for the production environment.
*
* Do not include API secrets in this file or anywhere in your JS.
*
* https://reactnative.dev/docs/security#storing-sensitive-info
*/
export default {
API_URL: "CHANGEME",
}

28
src/config/index.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* This file imports configuration objects from either the config.dev.js file
* or the config.prod.js file depending on whether we are in __DEV__ or not.
*
* Note that we do not gitignore these files. Unlike on web servers, just because
* these are not checked into your repo doesn't mean that they are secure.
* In fact, you're shipping a JavaScript bundle with every
* config variable in plain text. Anyone who downloads your app can easily
* extract them.
*
* If you doubt this, just bundle your app, and then go look at the bundle and
* search it for one of your config variable values. You'll find it there.
*
* Read more here: https://reactnative.dev/docs/security#storing-sensitive-info
*/
import BaseConfig from "./config.base"
import ProdConfig from "./config.prod"
import DevConfig from "./config.dev"
let ExtraConfig = ProdConfig
if (__DEV__) {
ExtraConfig = DevConfig
}
const Config = { ...BaseConfig, ...ExtraConfig }
export default Config

Some files were not shown because too many files have changed in this diff Show More