mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-05 20:03:28 -05:00
Add new item type default icons and placeholders (#1404)
This commit is contained in:
@@ -30,6 +30,7 @@ import net.aliasvault.app.autofill.models.FieldType
|
||||
import net.aliasvault.app.autofill.utils.CredentialMatcher
|
||||
import net.aliasvault.app.autofill.utils.FieldFinder
|
||||
import net.aliasvault.app.autofill.utils.ImageUtils
|
||||
import net.aliasvault.app.utils.ItemTypeIcon
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.interfaces.CredentialOperationCallback
|
||||
import net.aliasvault.app.vaultstore.models.Credential
|
||||
@@ -250,12 +251,8 @@ class AutofillService : AutofillService() {
|
||||
* @return The dataset
|
||||
*/
|
||||
private fun createCredentialDataset(fieldFinder: FieldFinder, credential: Credential): Dataset {
|
||||
// Choose layout based on whether we have a logo
|
||||
val layoutId = if (credential.service.logo != null) {
|
||||
R.layout.autofill_dataset_item_icon
|
||||
} else {
|
||||
R.layout.autofill_dataset_item
|
||||
}
|
||||
// Always use icon layout (will show logo or placeholder icon)
|
||||
val layoutId = R.layout.autofill_dataset_item_icon
|
||||
|
||||
// Create presentation for this credential using our custom layout
|
||||
val presentation = RemoteViews(packageName, layoutId)
|
||||
@@ -366,13 +363,21 @@ class AutofillService : AutofillService() {
|
||||
presentationDisplayValue,
|
||||
)
|
||||
|
||||
// Set the logo if available
|
||||
// Set the logo if available, otherwise use placeholder icon
|
||||
val logoBytes = credential.service.logo
|
||||
if (logoBytes != null) {
|
||||
val bitmap = ImageUtils.bytesToBitmap(logoBytes)
|
||||
if (bitmap != null) {
|
||||
presentation.setImageViewBitmap(R.id.icon, bitmap)
|
||||
}
|
||||
val bitmap = if (logoBytes != null) {
|
||||
ImageUtils.bytesToBitmap(logoBytes)
|
||||
} else {
|
||||
// Use placeholder key icon for Login/Alias items
|
||||
ItemTypeIcon.getIcon(
|
||||
context = this@AutofillService,
|
||||
itemType = ItemTypeIcon.ItemType.LOGIN,
|
||||
size = 96,
|
||||
)
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
presentation.setImageViewBitmap(R.id.icon, bitmap)
|
||||
}
|
||||
|
||||
return dataSetBuilder.build()
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
package net.aliasvault.app.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
|
||||
/**
|
||||
* Item type icon helper - provides bitmap-based icons for different item types
|
||||
* Matches the design from browser extension and iOS implementations
|
||||
*/
|
||||
object ItemTypeIcon {
|
||||
|
||||
/**
|
||||
* Item type enumeration matching the database model
|
||||
*/
|
||||
object ItemType {
|
||||
const val LOGIN = "Login"
|
||||
const val ALIAS = "Alias"
|
||||
const val CREDIT_CARD = "CreditCard"
|
||||
const val NOTE = "Note"
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit card brand type
|
||||
*/
|
||||
enum class CardBrand {
|
||||
VISA,
|
||||
MASTERCARD,
|
||||
AMEX,
|
||||
DISCOVER,
|
||||
GENERIC,
|
||||
;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Detect credit card brand from card number using industry-standard prefixes
|
||||
* @param cardNumber The card number to detect brand from
|
||||
* @return The detected card brand
|
||||
*/
|
||||
fun detect(cardNumber: String?): CardBrand {
|
||||
if (cardNumber.isNullOrEmpty()) {
|
||||
return GENERIC
|
||||
}
|
||||
|
||||
// Remove spaces and dashes
|
||||
val cleaned = cardNumber.replace(Regex("[\\s-]"), "")
|
||||
|
||||
// Must be mostly numeric
|
||||
if (!cleaned.matches(Regex("^\\d{4,}.*"))) {
|
||||
return GENERIC
|
||||
}
|
||||
|
||||
// Visa: starts with 4
|
||||
if (cleaned.matches(Regex("^4.*"))) {
|
||||
return VISA
|
||||
}
|
||||
|
||||
// Mastercard: starts with 51-55 or 2221-2720
|
||||
if (cleaned.matches(Regex("^5[1-5].*")) || cleaned.matches(Regex("^2[2-7].*"))) {
|
||||
return MASTERCARD
|
||||
}
|
||||
|
||||
// Amex: starts with 34 or 37
|
||||
if (cleaned.matches(Regex("^3[47].*"))) {
|
||||
return AMEX
|
||||
}
|
||||
|
||||
// Discover: starts with 6011, 622, 644-649, 65
|
||||
if (cleaned.matches(Regex("^6(?:011|22|4[4-9]|5).*"))) {
|
||||
return DISCOVER
|
||||
}
|
||||
|
||||
return GENERIC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AliasVault color scheme
|
||||
private const val COLOR_PRIMARY = "#f49541"
|
||||
private const val COLOR_DARK = "#d68338"
|
||||
private const val COLOR_LIGHT = "#ffe096"
|
||||
private const val COLOR_LIGHTER = "#fbcb74"
|
||||
|
||||
/**
|
||||
* Get the appropriate icon bitmap for an item type
|
||||
* @param context Android context
|
||||
* @param itemType The item type (Login, Alias, CreditCard, Note)
|
||||
* @param cardNumber Optional card number for credit card brand detection
|
||||
* @param size Icon size in pixels (default 96)
|
||||
* @return Bitmap icon
|
||||
*/
|
||||
fun getIcon(
|
||||
context: Context,
|
||||
itemType: String,
|
||||
cardNumber: String? = null,
|
||||
size: Int = 96,
|
||||
): Bitmap {
|
||||
return when (itemType) {
|
||||
ItemType.NOTE -> createNoteIcon(size)
|
||||
ItemType.CREDIT_CARD -> {
|
||||
val brand = CardBrand.detect(cardNumber)
|
||||
getCardIcon(brand, size)
|
||||
}
|
||||
ItemType.LOGIN, ItemType.ALIAS -> createPlaceholderIcon(size)
|
||||
else -> createPlaceholderIcon(size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate icon for a credit card brand
|
||||
*/
|
||||
fun getCardIcon(brand: CardBrand, size: Int = 96): Bitmap {
|
||||
return when (brand) {
|
||||
CardBrand.VISA -> createVisaIcon(size)
|
||||
CardBrand.MASTERCARD -> createMastercardIcon(size)
|
||||
CardBrand.AMEX -> createAmexIcon(size)
|
||||
CardBrand.DISCOVER -> createDiscoverIcon(size)
|
||||
CardBrand.GENERIC -> createCreditCardIcon(size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generic credit card icon
|
||||
*/
|
||||
private fun createCreditCardIcon(size: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
val scale = size / 32f
|
||||
|
||||
// Card background
|
||||
paint.color = Color.parseColor(COLOR_PRIMARY)
|
||||
canvas.drawRoundRect(
|
||||
RectF(2 * scale, 6 * scale, 30 * scale, 26 * scale),
|
||||
3 * scale,
|
||||
3 * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
// Magnetic stripe
|
||||
paint.color = Color.parseColor(COLOR_DARK)
|
||||
canvas.drawRect(2 * scale, 11 * scale, 30 * scale, 15 * scale, paint)
|
||||
|
||||
// Chip
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
canvas.drawRoundRect(
|
||||
RectF(5 * scale, 18 * scale, 13 * scale, 20 * scale),
|
||||
1 * scale,
|
||||
1 * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
// Number line
|
||||
paint.color = Color.parseColor(COLOR_LIGHTER)
|
||||
canvas.drawRoundRect(
|
||||
RectF(5 * scale, 22 * scale, 10 * scale, 23.5f * scale),
|
||||
0.75f * scale,
|
||||
0.75f * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Visa icon
|
||||
*/
|
||||
private fun createVisaIcon(size: Int): Bitmap {
|
||||
val bitmap = createCreditCardIcon(size)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
paint.textSize = 6 * (size / 32f)
|
||||
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
|
||||
canvas.drawText("VISA", 8 * (size / 32f), 18 * (size / 32f), paint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Mastercard icon
|
||||
*/
|
||||
private fun createMastercardIcon(size: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
val scale = size / 32f
|
||||
|
||||
// Card background
|
||||
paint.color = Color.parseColor(COLOR_PRIMARY)
|
||||
canvas.drawRoundRect(
|
||||
RectF(2 * scale, 6 * scale, 30 * scale, 26 * scale),
|
||||
3 * scale,
|
||||
3 * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
// Left circle
|
||||
paint.color = Color.parseColor(COLOR_DARK)
|
||||
canvas.drawCircle(13 * scale, 16 * scale, 5 * scale, paint)
|
||||
|
||||
// Right circle
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
canvas.drawCircle(19 * scale, 16 * scale, 5 * scale, paint)
|
||||
|
||||
// Overlap (simplified)
|
||||
paint.color = Color.parseColor(COLOR_LIGHTER)
|
||||
val path = Path()
|
||||
path.addCircle(16 * scale, 16 * scale, 3.5f * scale, Path.Direction.CW)
|
||||
canvas.drawPath(path, paint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Amex icon
|
||||
*/
|
||||
private fun createAmexIcon(size: Int): Bitmap {
|
||||
val bitmap = createCreditCardIcon(size)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
paint.textSize = 8 * (size / 32f)
|
||||
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
|
||||
canvas.drawText("AMEX", 16 * (size / 32f), 18 * (size / 32f), paint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Discover icon
|
||||
*/
|
||||
private fun createDiscoverIcon(size: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
val scale = size / 32f
|
||||
|
||||
// Card background
|
||||
paint.color = Color.parseColor(COLOR_PRIMARY)
|
||||
canvas.drawRoundRect(
|
||||
RectF(2 * scale, 6 * scale, 30 * scale, 26 * scale),
|
||||
3 * scale,
|
||||
3 * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
// Circle logo
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
canvas.drawCircle(20 * scale, 16 * scale, 4 * scale, paint)
|
||||
|
||||
// "DI" text
|
||||
paint.textSize = 6 * scale
|
||||
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
canvas.drawText("D", 7 * scale, 17 * scale, paint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create note/document icon
|
||||
*/
|
||||
private fun createNoteIcon(size: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
val scale = size / 32f
|
||||
|
||||
// Document body
|
||||
paint.color = Color.parseColor(COLOR_PRIMARY)
|
||||
val path = Path()
|
||||
path.moveTo(8 * scale, 4 * scale)
|
||||
path.lineTo(19 * scale, 4 * scale)
|
||||
path.lineTo(26 * scale, 11 * scale)
|
||||
path.lineTo(26 * scale, 26 * scale)
|
||||
path.lineTo(8 * scale, 26 * scale)
|
||||
path.close()
|
||||
canvas.drawPath(path, paint)
|
||||
|
||||
// Folded corner
|
||||
paint.color = Color.parseColor(COLOR_DARK)
|
||||
val cornerPath = Path()
|
||||
cornerPath.moveTo(19 * scale, 4 * scale)
|
||||
cornerPath.lineTo(19 * scale, 11 * scale)
|
||||
cornerPath.lineTo(26 * scale, 11 * scale)
|
||||
cornerPath.close()
|
||||
canvas.drawPath(cornerPath, paint)
|
||||
|
||||
// Text lines
|
||||
paint.color = Color.parseColor(COLOR_LIGHT)
|
||||
canvas.drawRoundRect(
|
||||
RectF(10 * scale, 14 * scale, 22 * scale, 15.5f * scale),
|
||||
0.75f * scale,
|
||||
0.75f * scale,
|
||||
paint,
|
||||
)
|
||||
canvas.drawRoundRect(
|
||||
RectF(10 * scale, 18 * scale, 20 * scale, 19.5f * scale),
|
||||
0.75f * scale,
|
||||
0.75f * scale,
|
||||
paint,
|
||||
)
|
||||
canvas.drawRoundRect(
|
||||
RectF(10 * scale, 22 * scale, 18 * scale, 23.5f * scale),
|
||||
0.75f * scale,
|
||||
0.75f * scale,
|
||||
paint,
|
||||
)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create placeholder key icon for Login/Alias
|
||||
*/
|
||||
private fun createPlaceholderIcon(size: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
paint.color = Color.parseColor(COLOR_PRIMARY)
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeWidth = 2.5f * (size / 32f)
|
||||
paint.strokeCap = Paint.Cap.ROUND
|
||||
|
||||
val scale = size / 32f
|
||||
|
||||
// Key bow (circular head)
|
||||
canvas.drawCircle(10 * scale, 10 * scale, 6.5f * scale, paint)
|
||||
|
||||
// Key hole in bow
|
||||
paint.strokeWidth = 2 * scale
|
||||
canvas.drawCircle(10 * scale, 10 * scale, 2.5f * scale, paint)
|
||||
|
||||
// Key shaft - diagonal
|
||||
paint.strokeWidth = 2.5f * scale
|
||||
canvas.drawLine(15 * scale, 15 * scale, 27 * scale, 27 * scale, paint)
|
||||
|
||||
// Key teeth - perpendicular to shaft
|
||||
canvas.drawLine(19 * scale, 19 * scale, 23 * scale, 15 * scale, paint)
|
||||
canvas.drawLine(24 * scale, 24 * scale, 28 * scale, 20 * scale, paint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export default function ItemDetailsScreen() : React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedView style={styles.header}>
|
||||
<ItemIcon logo={item.Logo} style={styles.logo} />
|
||||
<ItemIcon item={item} style={styles.logo} />
|
||||
<View style={styles.headerText}>
|
||||
<ThemedText type="title" style={styles.serviceName}>
|
||||
{item.Name}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -19,6 +17,7 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
|
||||
import { ConfirmDeleteModal } from '@/components/items/ConfirmDeleteModal';
|
||||
import { ItemIcon } from '@/components/items/ItemIcon';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
@@ -215,15 +214,6 @@ export default function RecentlyDeletedScreen(): React.ReactNode {
|
||||
marginRight: 12,
|
||||
width: 32,
|
||||
},
|
||||
itemLogoPlaceholder: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary + '20',
|
||||
borderRadius: 4,
|
||||
height: 32,
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
width: 32,
|
||||
},
|
||||
itemInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -284,16 +274,7 @@ export default function RecentlyDeletedScreen(): React.ReactNode {
|
||||
<View key={item.Id} style={styles.itemCard}>
|
||||
<View style={styles.itemContent}>
|
||||
{/* Item logo */}
|
||||
{item.Logo ? (
|
||||
<Image
|
||||
source={{ uri: `data:image/png;base64,${Buffer.from(item.Logo).toString('base64')}` }}
|
||||
style={styles.itemLogo}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.itemLogoPlaceholder}>
|
||||
<MaterialIcons name="lock" size={18} color={colors.primary} />
|
||||
</View>
|
||||
)}
|
||||
<ItemIcon item={item} style={styles.itemLogo} />
|
||||
|
||||
{/* Item info */}
|
||||
<View style={styles.itemInfo}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-len */
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { router } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -289,7 +290,7 @@ export function ItemCard({ item, onItemDelete }: ItemCardProps): React.ReactNode
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.itemContent}>
|
||||
<ItemIcon logo={item.Logo} style={styles.logo} />
|
||||
<ItemIcon item={item} style={styles.logo} />
|
||||
<View style={styles.itemInfo}>
|
||||
<View style={styles.serviceNameRow}>
|
||||
<Text style={styles.serviceName}>
|
||||
|
||||
@@ -1,42 +1,290 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { Image, ImageStyle, StyleSheet } from 'react-native';
|
||||
import { Image, ImageStyle, StyleSheet, View } from 'react-native';
|
||||
import Svg, { Circle, Path, Rect, Text as SvgText } from 'react-native-svg';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { ItemTypes, FieldKey } from '@/utils/dist/core/models/vault';
|
||||
|
||||
import servicePlaceholder from '@/assets/images/service-placeholder.webp';
|
||||
|
||||
/**
|
||||
* Item icon props.
|
||||
* Item icon props - supports both legacy logo-only mode and new item-based mode.
|
||||
*/
|
||||
type ItemIconProps = {
|
||||
/** Legacy: Logo bytes for Login/Alias items */
|
||||
logo?: Uint8Array | number[] | string | null;
|
||||
/** New: Full item object for type-aware icon rendering */
|
||||
item?: Item;
|
||||
style?: ImageStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Item icon component.
|
||||
* Credit card brand type
|
||||
*/
|
||||
export function ItemIcon({ logo, style }: ItemIconProps) : React.ReactNode {
|
||||
type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'generic';
|
||||
|
||||
/**
|
||||
* Detect credit card brand from card number using industry-standard prefixes
|
||||
* @param cardNumber - The card number to detect brand from
|
||||
* @returns The detected card brand
|
||||
*/
|
||||
const detectCardBrand = (cardNumber: string | undefined): CardBrand => {
|
||||
if (!cardNumber) {
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
// Remove spaces and dashes
|
||||
const cleaned = cardNumber.replace(/[\s-]/g, '');
|
||||
|
||||
// Must be mostly numeric
|
||||
if (!/^\d{4,}/.test(cleaned)) {
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
// Visa: starts with 4
|
||||
if (/^4/.test(cleaned)) {
|
||||
return 'visa';
|
||||
}
|
||||
|
||||
// Mastercard: starts with 51-55 or 2221-2720
|
||||
if (/^5[1-5]/.test(cleaned) || /^2[2-7]/.test(cleaned)) {
|
||||
return 'mastercard';
|
||||
}
|
||||
|
||||
// Amex: starts with 34 or 37
|
||||
if (/^3[47]/.test(cleaned)) {
|
||||
return 'amex';
|
||||
}
|
||||
|
||||
// Discover: starts with 6011, 622, 644-649, 65
|
||||
if (/^6(?:011|22|4[4-9]|5)/.test(cleaned)) {
|
||||
return 'discover';
|
||||
}
|
||||
|
||||
return 'generic';
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic credit card icon in AliasVault style
|
||||
*/
|
||||
const CreditCardIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541" />
|
||||
<Rect x="2" y="11" width="28" height="4" fill="#d68338" />
|
||||
<Rect x="5" y="18" width="8" height="2" rx="1" fill="#ffe096" />
|
||||
<Rect x="5" y="22" width="5" height="1.5" rx="0.75" fill="#fbcb74" />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Visa card icon in AliasVault style
|
||||
*/
|
||||
const VisaIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541" />
|
||||
<Path
|
||||
d="M13.5 13L11.5 19H10L8.5 14.5C8.5 14.5 8.35 14 8 14C7.65 14 7 13.8 7 13.8L7.05 13.5H9.5C9.85 13.5 10.15 13.75 10.2 14.1L10.8 17L12.5 13.5H13.5V13ZM15 19H14L15 13H16L15 19ZM20 13.5C20 13.5 19.4 13.3 18.7 13.3C17.35 13.3 16.4 14 16.4 15C16.4 15.8 17.1 16.2 17.65 16.5C18.2 16.8 18.4 17 18.4 17.2C18.4 17.5 18.05 17.7 17.6 17.7C17 17.7 16.5 17.5 16.5 17.5L16.3 18.7C16.3 18.7 16.9 19 17.7 19C19.2 19 20.1 18.2 20.1 17.1C20.1 15.7 18.4 15.6 18.4 15C18.4 14.7 18.7 14.5 19.15 14.5C19.6 14.5 20.1 14.7 20.1 14.7L20.3 13.5H20V13.5ZM24 19L23.1 13.5H22C21.7 13.5 21.45 13.7 21.35 13.95L19 19H20.5L20.8 18H22.7L22.9 19H24ZM21.2 17L22 14.5L22.45 17H21.2Z"
|
||||
fill="#ffe096"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Mastercard icon in AliasVault style
|
||||
*/
|
||||
const MastercardIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541" />
|
||||
<Circle cx="13" cy="16" r="5" fill="#d68338" />
|
||||
<Circle cx="19" cy="16" r="5" fill="#ffe096" />
|
||||
<Path
|
||||
d="M16 12.5C17.1 13.4 17.8 14.6 17.8 16C17.8 17.4 17.1 18.6 16 19.5C14.9 18.6 14.2 17.4 14.2 16C14.2 14.6 14.9 13.4 16 12.5Z"
|
||||
fill="#fbcb74"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Amex card icon in AliasVault style
|
||||
*/
|
||||
const AmexIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541" />
|
||||
<SvgText
|
||||
x="16"
|
||||
y="18"
|
||||
textAnchor="middle"
|
||||
fill="#ffe096"
|
||||
fontSize="8"
|
||||
fontWeight="bold"
|
||||
fontFamily="Arial, sans-serif"
|
||||
>
|
||||
AMEX
|
||||
</SvgText>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Discover card icon in AliasVault style
|
||||
*/
|
||||
const DiscoverIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541" />
|
||||
<Circle cx="20" cy="16" r="4" fill="#ffe096" />
|
||||
<Path
|
||||
d="M7 14H8.5C9.3 14 10 14.7 10 15.5C10 16.3 9.3 17 8.5 17H7V14Z"
|
||||
fill="#ffe096"
|
||||
/>
|
||||
<Rect x="11" y="14" width="1.5" height="3" fill="#ffe096" />
|
||||
<Path
|
||||
d="M14 15C14 14.4 14.4 14 15 14C15.3 14 15.5 14.1 15.7 14.3L16.5 13.5C16.1 13.2 15.6 13 15 13C13.9 13 13 13.9 13 15C13 16.1 13.9 17 15 17C15.6 17 16.1 16.8 16.5 16.5L15.7 15.7C15.5 15.9 15.3 16 15 16C14.4 16 14 15.6 14 15Z"
|
||||
fill="#ffe096"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Note/document icon in AliasVault style
|
||||
*/
|
||||
const NoteIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
<Path
|
||||
d="M8 4C6.9 4 6 4.9 6 6V26C6 27.1 6.9 28 8 28H24C25.1 28 26 27.1 26 26V11L19 4H8Z"
|
||||
fill="#f49541"
|
||||
/>
|
||||
<Path d="M19 4V11H26L19 4Z" fill="#d68338" />
|
||||
<Rect x="10" y="14" width="12" height="1.5" rx="0.75" fill="#ffe096" />
|
||||
<Rect x="10" y="18" width="10" height="1.5" rx="0.75" fill="#ffe096" />
|
||||
<Rect x="10" y="22" width="8" height="1.5" rx="0.75" fill="#ffe096" />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Placeholder icon for Login/Alias items - traditional key design with outline style
|
||||
*/
|
||||
const PlaceholderIcon = ({ width = 32, height = 32 }: { width?: number; height?: number }) => (
|
||||
<Svg width={width} height={height} viewBox="0 0 32 32" fill="none">
|
||||
{/* Key bow (circular head) - positioned top-left */}
|
||||
<Circle cx="10" cy="10" r="6.5" stroke="#f49541" strokeWidth="2.5" />
|
||||
{/* Key hole in bow */}
|
||||
<Circle cx="10" cy="10" r="2.5" stroke="#f49541" strokeWidth="2" />
|
||||
{/* Key shaft - diagonal */}
|
||||
<Path d="M15 15L27 27" stroke="#f49541" strokeWidth="2.5" strokeLinecap="round" />
|
||||
{/* Key teeth - perpendicular to shaft */}
|
||||
<Path d="M19 19L23 15" stroke="#f49541" strokeWidth="2.5" strokeLinecap="round" />
|
||||
<Path d="M24 24L28 20" stroke="#f49541" strokeWidth="2.5" strokeLinecap="round" />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the appropriate icon component based on card brand
|
||||
*/
|
||||
const getCardIcon = (brand: CardBrand) => {
|
||||
switch (brand) {
|
||||
case 'visa':
|
||||
return VisaIcon;
|
||||
case 'mastercard':
|
||||
return MastercardIcon;
|
||||
case 'amex':
|
||||
return AmexIcon;
|
||||
case 'discover':
|
||||
return DiscoverIcon;
|
||||
default:
|
||||
return CreditCardIcon;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Item icon component - supports both item-based and legacy logo-based rendering.
|
||||
*/
|
||||
export function ItemIcon({ logo, item, style }: ItemIconProps) : React.ReactNode {
|
||||
const width = Number(style?.width ?? styles.logo.width);
|
||||
const height = Number(style?.height ?? styles.logo.height);
|
||||
|
||||
// New item-based rendering mode
|
||||
if (item) {
|
||||
// For Note type, always show note icon
|
||||
if (item.ItemType === ItemTypes.Note) {
|
||||
return (
|
||||
<View style={[styles.iconContainer, style]}>
|
||||
<NoteIcon width={width} height={height} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// For CreditCard type, detect card brand and show appropriate icon
|
||||
if (item.ItemType === ItemTypes.CreditCard) {
|
||||
const cardNumberField = item.Fields?.find(f => f.FieldKey === FieldKey.CardNumber);
|
||||
const cardNumber = cardNumberField?.Value
|
||||
? (Array.isArray(cardNumberField.Value) ? cardNumberField.Value[0] : cardNumberField.Value)
|
||||
: undefined;
|
||||
|
||||
const brand = detectCardBrand(cardNumber);
|
||||
const CardIcon = getCardIcon(brand);
|
||||
|
||||
return (
|
||||
<View style={[styles.iconContainer, style]}>
|
||||
<CardIcon width={width} height={height} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// For Login/Alias types, use Logo if available, otherwise placeholder
|
||||
const logoData = item.Logo;
|
||||
if (logoData && logoData.length > 0) {
|
||||
return renderLogo(logoData, style);
|
||||
}
|
||||
|
||||
// Default placeholder for Login/Alias without logo
|
||||
return (
|
||||
<View style={[styles.iconContainer, style]}>
|
||||
<PlaceholderIcon width={width} height={height} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy logo-only rendering mode
|
||||
if (logo && (typeof logo === 'string' || logo.length > 0)) {
|
||||
return renderLogo(logo, style);
|
||||
}
|
||||
|
||||
// Fallback to placeholder
|
||||
return (
|
||||
<View style={[styles.iconContainer, style]}>
|
||||
<PlaceholderIcon width={width} height={height} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render logo from binary data.
|
||||
*/
|
||||
function renderLogo(
|
||||
logoData: Uint8Array | number[] | string,
|
||||
style?: ImageStyle
|
||||
): React.ReactNode {
|
||||
/**
|
||||
* Get the logo source.
|
||||
*/
|
||||
const getLogoSource = (logoData: Uint8Array | number[] | string | null | undefined) : { type: 'image' | 'svg', source: string | number } => {
|
||||
if (!logoData) {
|
||||
const getLogoSource = (data: Uint8Array | number[] | string | null | undefined) : { type: 'image' | 'svg', source: string | number } => {
|
||||
if (!data) {
|
||||
return { type: 'image', source: servicePlaceholder };
|
||||
}
|
||||
|
||||
try {
|
||||
// If logo is already a base64 string (from iOS SQLite query result)
|
||||
if (typeof logoData === 'string') {
|
||||
const mimeType = detectMimeTypeFromBase64(logoData);
|
||||
if (typeof data === 'string') {
|
||||
const mimeType = detectMimeTypeFromBase64(data);
|
||||
return {
|
||||
type: mimeType === 'image/svg+xml' ? 'svg' : 'image',
|
||||
source: `data:${mimeType};base64,${logoData}`
|
||||
source: `data:${mimeType};base64,${data}`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle binary data (from Android or other sources)
|
||||
const logoBytes = toUint8Array(logoData);
|
||||
const logoBytes = toUint8Array(data);
|
||||
const base64Logo = Buffer.from(logoBytes).toString('base64');
|
||||
const mimeType = detectMimeType(logoBytes);
|
||||
return {
|
||||
@@ -49,7 +297,7 @@ export function ItemIcon({ logo, style }: ItemIconProps) : React.ReactNode {
|
||||
}
|
||||
};
|
||||
|
||||
const logoSource = getLogoSource(logo);
|
||||
const logoSource = getLogoSource(logoData);
|
||||
|
||||
if (logoSource.type === 'svg') {
|
||||
/*
|
||||
@@ -166,4 +414,8 @@ const styles = StyleSheet.create({
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
iconContainer: {
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,23 @@
|
||||
import SwiftUI
|
||||
import Macaw
|
||||
|
||||
/// Service logo view
|
||||
public struct ServiceLogoView: View {
|
||||
/// Item logo view - displays logos or type-based icons for items
|
||||
public struct ItemLogoView: View {
|
||||
|
||||
private let placeholderImageBase64 = "UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==" // swiftlint:disable:this line_length
|
||||
|
||||
let logoData: Data?
|
||||
let itemType: String?
|
||||
let cardNumber: String?
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
public init(logoData: Data?, itemType: String? = nil, cardNumber: String? = nil) {
|
||||
self.logoData = logoData
|
||||
self.itemType = itemType
|
||||
self.cardNumber = cardNumber
|
||||
}
|
||||
|
||||
private var colors: ColorConstants.Colors.Type {
|
||||
ColorConstants.colors(for: colorScheme)
|
||||
}
|
||||
@@ -66,32 +75,91 @@ public struct ServiceLogoView: View {
|
||||
|
||||
public var body: some View {
|
||||
Group {
|
||||
if let logoData = logoData {
|
||||
let mimeType = detectMimeType(logoData)
|
||||
if mimeType == "image/svg+xml",
|
||||
let svgNode = renderSVGNode(logoData) {
|
||||
SVGImageView(node: svgNode)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else if let image = UIImage(data: logoData) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else if let placeholder = placeholderImage {
|
||||
Image(uiImage: placeholder)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
// If itemType is specified, use type-based rendering
|
||||
if let itemType = itemType {
|
||||
renderTypeBasedIcon(itemType: itemType)
|
||||
} else if let logoData = logoData {
|
||||
// Legacy logo rendering
|
||||
renderLogo(logoData: logoData)
|
||||
} else {
|
||||
// Fallback to placeholder
|
||||
renderPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render icon based on item type
|
||||
private func renderTypeBasedIcon(itemType: String) -> some View {
|
||||
Group {
|
||||
// For Note type, always show note icon
|
||||
if itemType == ItemTypeIcon.ItemType.note.rawValue {
|
||||
renderSVGIcon(svg: ItemTypeIcon.noteIcon)
|
||||
}
|
||||
// For CreditCard type, detect brand and show appropriate icon
|
||||
else if itemType == ItemTypeIcon.ItemType.creditCard.rawValue {
|
||||
let brand = ItemTypeIcon.CardBrand.detect(from: cardNumber)
|
||||
let cardIcon = ItemTypeIcon.getCardIcon(for: brand)
|
||||
renderSVGIcon(svg: cardIcon)
|
||||
}
|
||||
// For Login/Alias types, use Logo if available, otherwise placeholder
|
||||
else if let logoData = logoData, !logoData.isEmpty {
|
||||
renderLogo(logoData: logoData)
|
||||
} else {
|
||||
renderSVGIcon(svg: ItemTypeIcon.placeholderIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an SVG icon from string
|
||||
private func renderSVGIcon(svg: String) -> some View {
|
||||
Group {
|
||||
if let svgData = svg.data(using: .utf8),
|
||||
let svgNode = try? SVGParser.parse(text: svg) {
|
||||
SVGImageView(node: svgNode)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else {
|
||||
renderPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render logo from binary data
|
||||
private func renderLogo(logoData: Data) -> some View {
|
||||
Group {
|
||||
let mimeType = detectMimeType(logoData)
|
||||
if mimeType == "image/svg+xml",
|
||||
let svgNode = renderSVGNode(logoData) {
|
||||
SVGImageView(node: svgNode)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else if let image = UIImage(data: logoData) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else if let placeholder = placeholderImage {
|
||||
Image(uiImage: placeholder)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else {
|
||||
renderPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render fallback placeholder
|
||||
private func renderPlaceholder() -> some View {
|
||||
Group {
|
||||
if let placeholder = placeholderImage {
|
||||
Image(uiImage: placeholder)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else {
|
||||
// Ultimate fallback if placeholder fails to load
|
||||
Circle()
|
||||
@@ -108,5 +176,5 @@ public struct ServiceLogoView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ServiceLogoView(logoData: nil)
|
||||
ItemLogoView(logoData: nil)
|
||||
}
|
||||
159
apps/mobile-app/ios/VaultUI/Components/ItemTypeIcon.swift
Normal file
159
apps/mobile-app/ios/VaultUI/Components/ItemTypeIcon.swift
Normal file
@@ -0,0 +1,159 @@
|
||||
// swiftlint:disable line_length
|
||||
import SwiftUI
|
||||
|
||||
/// Item type icon helper - provides SVG-based icons for different item types
|
||||
public struct ItemTypeIcon {
|
||||
|
||||
/// Item type enumeration matching the database model
|
||||
public enum ItemType: String {
|
||||
case login = "Login"
|
||||
case alias = "Alias"
|
||||
case creditCard = "CreditCard"
|
||||
case note = "Note"
|
||||
}
|
||||
|
||||
/// Credit card brand type
|
||||
public enum CardBrand {
|
||||
case visa
|
||||
case mastercard
|
||||
case amex
|
||||
case discover
|
||||
case generic
|
||||
|
||||
/// Detect credit card brand from card number using industry-standard prefixes
|
||||
public static func detect(from cardNumber: String?) -> CardBrand {
|
||||
guard let cardNumber = cardNumber else {
|
||||
return .generic
|
||||
}
|
||||
|
||||
// Remove spaces and dashes
|
||||
let cleaned = cardNumber.replacingOccurrences(of: "[\\s-]", with: "", options: .regularExpression)
|
||||
|
||||
// Must be mostly numeric
|
||||
guard cleaned.range(of: "^\\d{4,}", options: .regularExpression) != nil else {
|
||||
return .generic
|
||||
}
|
||||
|
||||
// Visa: starts with 4
|
||||
if cleaned.hasPrefix("4") {
|
||||
return .visa
|
||||
}
|
||||
|
||||
// Mastercard: starts with 51-55 or 2221-2720
|
||||
if cleaned.range(of: "^5[1-5]", options: .regularExpression) != nil ||
|
||||
cleaned.range(of: "^2[2-7]", options: .regularExpression) != nil {
|
||||
return .mastercard
|
||||
}
|
||||
|
||||
// Amex: starts with 34 or 37
|
||||
if cleaned.range(of: "^3[47]", options: .regularExpression) != nil {
|
||||
return .amex
|
||||
}
|
||||
|
||||
// Discover: starts with 6011, 622, 644-649, 65
|
||||
if cleaned.range(of: "^6(?:011|22|4[4-9]|5)", options: .regularExpression) != nil {
|
||||
return .discover
|
||||
}
|
||||
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic credit card icon SVG
|
||||
public static let creditCardIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541"/>
|
||||
<rect x="2" y="11" width="28" height="4" fill="#d68338"/>
|
||||
<rect x="5" y="18" width="8" height="2" rx="1" fill="#ffe096"/>
|
||||
<rect x="5" y="22" width="5" height="1.5" rx="0.75" fill="#fbcb74"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Visa card icon SVG
|
||||
public static let visaIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541"/>
|
||||
<path d="M13.5 13L11.5 19H10L8.5 14.5C8.5 14.5 8.35 14 8 14C7.65 14 7 13.8 7 13.8L7.05 13.5H9.5C9.85 13.5 10.15 13.75 10.2 14.1L10.8 17L12.5 13.5H13.5V13ZM15 19H14L15 13H16L15 19ZM20 13.5C20 13.5 19.4 13.3 18.7 13.3C17.35 13.3 16.4 14 16.4 15C16.4 15.8 17.1 16.2 17.65 16.5C18.2 16.8 18.4 17 18.4 17.2C18.4 17.5 18.05 17.7 17.6 17.7C17 17.7 16.5 17.5 16.5 17.5L16.3 18.7C16.3 18.7 16.9 19 17.7 19C19.2 19 20.1 18.2 20.1 17.1C20.1 15.7 18.4 15.6 18.4 15C18.4 14.7 18.7 14.5 19.15 14.5C19.6 14.5 20.1 14.7 20.1 14.7L20.3 13.5H20V13.5ZM24 19L23.1 13.5H22C21.7 13.5 21.45 13.7 21.35 13.95L19 19H20.5L20.8 18H22.7L22.9 19H24ZM21.2 17L22 14.5L22.45 17H21.2Z" fill="#ffe096"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Mastercard icon SVG
|
||||
public static let mastercardIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541"/>
|
||||
<circle cx="13" cy="16" r="5" fill="#d68338"/>
|
||||
<circle cx="19" cy="16" r="5" fill="#ffe096"/>
|
||||
<path d="M16 12.5C17.1 13.4 17.8 14.6 17.8 16C17.8 17.4 17.1 18.6 16 19.5C14.9 18.6 14.2 17.4 14.2 16C14.2 14.6 14.9 13.4 16 12.5Z" fill="#fbcb74"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Amex card icon SVG
|
||||
public static let amexIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541"/>
|
||||
<text x="16" y="18" text-anchor="middle" fill="#ffe096" font-size="8" font-weight="bold" font-family="Arial, sans-serif">AMEX</text>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Discover card icon SVG
|
||||
public static let discoverIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="6" width="28" height="20" rx="3" fill="#f49541"/>
|
||||
<circle cx="20" cy="16" r="4" fill="#ffe096"/>
|
||||
<path d="M7 14H8.5C9.3 14 10 14.7 10 15.5C10 16.3 9.3 17 8.5 17H7V14Z" fill="#ffe096"/>
|
||||
<rect x="11" y="14" width="1.5" height="3" fill="#ffe096"/>
|
||||
<path d="M14 15C14 14.4 14.4 14 15 14C15.3 14 15.5 14.1 15.7 14.3L16.5 13.5C16.1 13.2 15.6 13 15 13C13.9 13 13 13.9 13 15C13 16.1 13.9 17 15 17C15.6 17 16.1 16.8 16.5 16.5L15.7 15.7C15.5 15.9 15.3 16 15 16C14.4 16 14 15.6 14 15Z" fill="#ffe096"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Note/document icon SVG
|
||||
public static let noteIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4C6.9 4 6 4.9 6 6V26C6 27.1 6.9 28 8 28H24C25.1 28 26 27.1 26 26V11L19 4H8Z" fill="#f49541"/>
|
||||
<path d="M19 4V11H26L19 4Z" fill="#d68338"/>
|
||||
<rect x="10" y="14" width="12" height="1.5" rx="0.75" fill="#ffe096"/>
|
||||
<rect x="10" y="18" width="10" height="1.5" rx="0.75" fill="#ffe096"/>
|
||||
<rect x="10" y="22" width="8" height="1.5" rx="0.75" fill="#ffe096"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Placeholder key icon SVG for Login/Alias without logo
|
||||
public static let placeholderIcon = """
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="6.5" stroke="#f49541" stroke-width="2.5"/>
|
||||
<circle cx="10" cy="10" r="2.5" stroke="#f49541" stroke-width="2"/>
|
||||
<path d="M15 15L27 27" stroke="#f49541" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M19 19L23 15" stroke="#f49541" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M24 24L28 20" stroke="#f49541" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
/// Get the appropriate SVG icon for a credit card brand
|
||||
public static func getCardIcon(for brand: CardBrand) -> String {
|
||||
switch brand {
|
||||
case .visa:
|
||||
return visaIcon
|
||||
case .mastercard:
|
||||
return mastercardIcon
|
||||
case .amex:
|
||||
return amexIcon
|
||||
case .discover:
|
||||
return discoverIcon
|
||||
case .generic:
|
||||
return creditCardIcon
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the appropriate SVG icon for an item type
|
||||
public static func getIcon(for itemType: ItemType, cardNumber: String? = nil) -> String {
|
||||
switch itemType {
|
||||
case .note:
|
||||
return noteIcon
|
||||
case .creditCard:
|
||||
let brand = CardBrand.detect(from: cardNumber)
|
||||
return getCardIcon(for: brand)
|
||||
case .login, .alias:
|
||||
return placeholderIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public struct AutofillCredentialCard: View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 16) {
|
||||
// Service logo
|
||||
ServiceLogoView(logoData: credential.logo)
|
||||
ItemLogoView(logoData: credential.logo)
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
||||
Reference in New Issue
Block a user